
Pdf Generator
- 901 installs
- 135 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
pdf-generator is a Deno-based generator skill that creates, fills, merges, watermarks, and extracts PDF documents programmatically for developers who need agent-driven document automation in workflows.
About
pdf-generator is an MIT-licensed agent skill (version 1.0) that creates and manipulates PDF files programmatically from agent workflows. It supports template-based generation including form filling and overlays, plus from-scratch PDF creation, merging multiple documents, adding watermarks, and extracting text content. The skill runs on Deno and requires --allow-read and --allow-write permissions for filesystem access. Developers reach for pdf-generator when an agent must output invoices, filled government forms, merged report packets, or watermarked drafts without leaving the coding session. Keywords in the manifest include PDF, form, fillable, merge, watermark, extract, and report.
- Creates PDFs from scratch using JSON specifications
- Fills existing PDF forms with dynamic data including text, checkboxes, and dropdowns
- Adds watermarks, stamps, and overlays to existing PDFs
- Extracts text, metadata, and analyzes PDF structure and form fields
- Merges multiple PDF documents into a single output file
Pdf Generator by the numbers
- 901 all-time installs (skills.sh)
- +4 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #286 of 1,877 Documentation skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 6, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/agent-skills --skill pdf-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 901 |
|---|---|
| repo stars | ★ 135 |
| Security audit | 3 / 3 scanners passed |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
How do you programmatically fill and merge PDF forms?
Generate, fill, merge, watermark, or extract content from PDF documents directly from agent workflows.
Who is it for?
Developers automating report generation, form filling, or document merging who already run Deno with filesystem permissions.
Skip if: Teams needing a GUI PDF editor, complex layout design in InDesign, or environments where Deno runtime access is blocked.
When should I use this skill?
The user needs to generate a PDF, fill a PDF form, merge documents, add watermarks, or extract text from PDF files.
What you get
Generated PDF files with filled form fields, merged pages, watermarks, overlays, or extracted text content on disk.
- filled PDF forms
- merged PDF documents
- watermarked PDF files
By the numbers
- Skill version 1.0 with MIT license in manifest metadata
Files
PDF Generator
When to Use This Skill
Use this skill when:
- Creating PDF documents programmatically from data or specifications
- Filling PDF forms with dynamic data
- Adding watermarks, stamps, or overlays to existing PDFs
- Extracting text and metadata from PDF files
- Merging multiple PDFs into one document
- Analyzing PDF structure and form fields
Do NOT use this skill when:
- User wants to open/view PDFs (use native PDF viewer)
- Complex page layout with flowing text is needed (consider HTML-to-PDF tools)
- Working with password-protected PDFs (limited support)
- OCR is needed for scanned documents
Prerequisites
- Deno installed (https://deno.land/)
- Input PDF files for template-based operations
- JSON specification for scratch generation
Quick Start
Two Modes of Operation
1. Template Mode: Modify existing PDF templates
- Fill form fields (text, checkbox, dropdown)
- Add overlays (text, images, shapes)
- Merge and combine PDFs
2. Scratch Mode: Create PDFs from nothing using JSON specifications
Instructions
Mode 1: Template-Based Generation
Step 1a: Analyze the Template
Extract form fields and structure from an existing PDF:
deno run --allow-read scripts/analyze-template.ts form-template.pdf > inventory.jsonOutput (inventory.json):
{
"filename": "form-template.pdf",
"pageCount": 2,
"title": "Application Form",
"author": "Company Inc",
"pages": [
{ "pageNumber": 1, "width": 612, "height": 792, "text": "..." }
],
"formFields": [
{ "name": "FullName", "type": "text", "value": "" },
{ "name": "Email", "type": "text", "value": "" },
{ "name": "AgreeToTerms", "type": "checkbox", "value": false }
],
"placeholders": [
{ "tag": "{{DATE}}", "location": "page 1", "pageNumber": 1 }
],
"hasFormFields": true
}Step 1b: Create Fill Specification
Create form-data.json:
{
"formFields": [
{ "name": "FullName", "value": "John Smith" },
{ "name": "Email", "value": "john@example.com" },
{ "name": "AgreeToTerms", "value": true }
],
"flattenForm": true
}Step 1c: Generate Filled PDF
deno run --allow-read --allow-write scripts/generate-from-template.ts \
form-template.pdf form-data.json filled-form.pdfAdding Overlays (Watermarks, Stamps)
Create watermark-spec.json:
{
"overlays": [
{
"type": "text",
"page": 1,
"x": 200,
"y": 400,
"text": "CONFIDENTIAL",
"fontSize": 48,
"color": { "r": 1, "g": 0, "b": 0 },
"rotate": 45
},
{
"type": "image",
"page": 1,
"x": 450,
"y": 700,
"path": "logo.png",
"width": 100,
"height": 50
}
]
}Merging PDFs
Create merge-spec.json:
{
"prependPdfs": [
{ "path": "cover-page.pdf" }
],
"appendPdfs": [
{ "path": "appendix-a.pdf", "pages": [1, 2, 3] },
{ "path": "appendix-b.pdf" }
],
"excludePages": [5, 6]
}Mode 2: From-Scratch Generation
Step 2a: Create Specification
Create spec.json:
{
"title": "Quarterly Report",
"author": "Finance Team",
"pages": [
{
"size": "A4",
"elements": [
{
"type": "text",
"x": 50,
"y": 750,
"text": "Q4 2024 Financial Report",
"fontSize": 28,
"font": "HelveticaBold",
"color": { "r": 0, "g": 0, "b": 0.5 }
},
{
"type": "line",
"startX": 50,
"startY": 740,
"endX": 550,
"endY": 740,
"thickness": 2
},
{
"type": "text",
"x": 50,
"y": 700,
"text": "Executive Summary",
"fontSize": 18,
"font": "HelveticaBold"
},
{
"type": "text",
"x": 50,
"y": 670,
"text": "This quarter showed strong growth across all divisions...",
"fontSize": 12,
"maxWidth": 500,
"lineHeight": 16
}
]
}
]
}Step 2b: Generate PDF
deno run --allow-read --allow-write scripts/generate-scratch.ts spec.json output.pdfExamples
Example 1: Fill Application Form
Scenario: Automatically fill a job application form.
# 1. Analyze form to find field names
deno run --allow-read scripts/analyze-template.ts application.pdf --pretty
# 2. Create form-data.json with applicant info
# 3. Generate filled form
deno run --allow-read --allow-write scripts/generate-from-template.ts \
application.pdf form-data.json john-smith-application.pdfExample 2: Add Approval Stamp
Scenario: Add an "APPROVED" stamp to a document.
stamp-spec.json:
{
"overlays": [
{
"type": "rectangle",
"page": 1,
"x": 400,
"y": 700,
"width": 150,
"height": 50,
"color": { "r": 0.9, "g": 1, "b": 0.9 }
},
{
"type": "text",
"page": 1,
"x": 410,
"y": 720,
"text": "APPROVED",
"fontSize": 20,
"font": "HelveticaBold",
"color": { "r": 0, "g": 0.5, "b": 0 }
},
{
"type": "text",
"page": 1,
"x": 410,
"y": 705,
"text": "2024-12-15",
"fontSize": 10
}
]
}Example 3: Create Report with Table
Scenario: Generate a simple report with a data table.
report-spec.json:
{
"title": "Sales Report",
"pages": [{
"size": "Letter",
"elements": [
{
"type": "text",
"x": 72,
"y": 720,
"text": "Monthly Sales Report",
"fontSize": 24,
"font": "HelveticaBold"
},
{
"type": "table",
"x": 72,
"y": 680,
"rows": [
["Product", "Units", "Revenue"],
["Widget A", "150", "$15,000"],
["Widget B", "75", "$11,250"],
["Widget C", "200", "$8,000"]
],
"columnWidths": [150, 80, 100],
"rowHeight": 25,
"headerBackground": { "r": 0.9, "g": 0.9, "b": 0.9 }
}
]
}]
}Script Reference
| Script | Purpose | Permissions |
|---|---|---|
analyze-template.ts | Extract text, metadata, form fields from PDF | --allow-read |
generate-from-template.ts | Fill forms, add overlays, merge PDFs | --allow-read --allow-write |
generate-scratch.ts | Create PDF from JSON specification | --allow-read --allow-write |
Element Types (Scratch Mode)
| Type | Description | Key Options |
|---|---|---|
text | Text content | x, y, text, fontSize, font, color, rotate |
image | PNG/JPEG images | x, y, path, width, height, opacity |
rectangle | Filled/outlined rectangles | x, y, width, height, color, borderColor |
line | Straight lines | startX, startY, endX, endY, thickness |
circle | Filled/outlined circles | x, y, radius, color, borderColor |
table | Basic table layout | x, y, rows, columnWidths, rowHeight |
Available Fonts
Helvetica(default)HelveticaBoldHelveticaObliqueTimesRomanTimesBoldCourierCourierBold
Page Sizes
A4(595.28 x 841.89 points)Letter(612 x 792 points)Legal(612 x 1008 points)- Custom:
[width, height]in points
Common Issues and Solutions
Issue: Form fields not found
Symptoms: Error saying field name doesn't exist.
Solution: 1. Run analyze-template.ts to get exact field names 2. Field names are case-sensitive 3. Some PDFs have non-fillable text that looks like form fields
Issue: Text positioning is off
Symptoms: Text appears in wrong location.
Solution:
- PDF coordinates start from bottom-left (0,0)
- Y increases upward, X increases rightward
- Use
analyze-template.tsto see page dimensions
Issue: Images not appearing
Symptoms: Image overlay not visible.
Solution: 1. Check file path is relative to spec.json location 2. Verify image is PNG or JPEG format 3. Ensure coordinates are within page bounds
Issue: Merged PDF has wrong page order
Symptoms: Pages appear in unexpected order.
Solution:
prependPdfsadd pages before templateappendPdfsadd pages after template- Use
pagesarray to select specific pages:[1, 3, 5]
Limitations
- No built-in table layout: Tables require manual column width specification
- Standard fonts only: Custom font embedding not supported in scratch mode
- No flowing text: Text doesn't automatically wrap to next page
- Limited form field creation: Can fill existing forms, not create new fields
- No encryption: Cannot create password-protected PDFs
- Basic graphics: No gradients, patterns, or complex paths
- Text extraction: May not work perfectly on all PDFs (depends on PDF structure)
Related Skills
- pptx-generator: For creating PowerPoint presentations
- docx-generator: For creating Word documents
- xlsx-generator: For creating Excel spreadsheets
#!/usr/bin/env -S deno run --allow-read
/**
* analyze-template.ts - Extract content and structure from PDF files
*
* Extracts text, metadata, form fields, and page information from PDF
* documents for template analysis and content planning.
*
* Usage:
* deno run --allow-read scripts/analyze-template.ts <input.pdf> [options]
*
* Options:
* -h, --help Show help
* -v, --verbose Enable verbose output
* --pretty Pretty-print JSON output
* --page <num> Analyze only specific page (1-indexed)
*
* Permissions:
* --allow-read: Read PDF file
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { basename } from "jsr:@std/path@1.0.8";
import { extractText, getDocumentProxy } from "npm:unpdf@0.11.0";
import { PDFDocument } from "npm:pdf-lib@1.17.1";
// === Types ===
export interface FormFieldInfo {
name: string;
type: "text" | "checkbox" | "radio" | "dropdown" | "button" | "signature" | "unknown";
value?: string | boolean;
options?: string[];
required?: boolean;
readOnly?: boolean;
}
export interface PageInfo {
pageNumber: number;
width: number;
height: number;
text: string;
rotation: number;
}
export interface PlaceholderInfo {
tag: string;
location: string;
pageNumber: number;
}
export interface PDFInventory {
filename: string;
pageCount: number;
title?: string;
author?: string;
subject?: string;
creator?: string;
producer?: string;
creationDate?: string;
modificationDate?: string;
pages: PageInfo[];
formFields: FormFieldInfo[];
placeholders: PlaceholderInfo[];
hasFormFields: boolean;
isEncrypted: boolean;
}
interface ParsedArgs {
help: boolean;
verbose: boolean;
pretty: boolean;
page?: number;
_: (string | number)[];
}
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "analyze-template";
// Placeholder patterns: {{PLACEHOLDER}} or ${placeholder}
const PLACEHOLDER_REGEX = /\{\{([^}]+)\}\}|\$\{([^}]+)\}/g;
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Extract content and structure from PDF files
Usage:
deno run --allow-read scripts/${SCRIPT_NAME}.ts <input.pdf> [options]
Arguments:
<input.pdf> Path to the PDF file to analyze
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output (to stderr)
--pretty Pretty-print JSON output (default: compact)
--page <num> Analyze only specific page (1-indexed)
Examples:
# Analyze PDF
deno run --allow-read scripts/${SCRIPT_NAME}.ts document.pdf > inventory.json
# With verbose output
deno run --allow-read scripts/${SCRIPT_NAME}.ts document.pdf -v --pretty
# Analyze specific page
deno run --allow-read scripts/${SCRIPT_NAME}.ts document.pdf --page 1
`);
}
// === Utility Functions ===
function findPlaceholders(text: string, pageNumber: number): PlaceholderInfo[] {
const placeholders: PlaceholderInfo[] = [];
let match;
const regex = new RegExp(PLACEHOLDER_REGEX.source, "g");
while ((match = regex.exec(text)) !== null) {
placeholders.push({
tag: match[0],
location: `page ${pageNumber}`,
pageNumber,
});
}
return placeholders;
}
function formatDate(date: Date | undefined): string | undefined {
if (!date) return undefined;
try {
return date.toISOString();
} catch {
return undefined;
}
}
// === Core Logic ===
export async function analyzePDF(
pdfPath: string,
options: { verbose?: boolean; pageFilter?: number } = {}
): Promise<PDFInventory> {
const { verbose = false, pageFilter } = options;
// Read the PDF file
const data = await Deno.readFile(pdfPath);
const filename = basename(pdfPath);
if (verbose) {
console.error(`Analyzing: ${filename}`);
}
// Load with pdf-lib for metadata and form fields
let pdfDoc: PDFDocument;
let isEncrypted = false;
try {
pdfDoc = await PDFDocument.load(data, { ignoreEncryption: true });
} catch (error) {
if (String(error).includes("encrypted")) {
isEncrypted = true;
// Try loading without encryption check
pdfDoc = await PDFDocument.load(data, { ignoreEncryption: true });
} else {
throw error;
}
}
const pageCount = pdfDoc.getPageCount();
if (verbose) {
console.error(`Pages: ${pageCount}`);
}
// Extract metadata
const title = pdfDoc.getTitle();
const author = pdfDoc.getAuthor();
const subject = pdfDoc.getSubject();
const creator = pdfDoc.getCreator();
const producer = pdfDoc.getProducer();
const creationDate = formatDate(pdfDoc.getCreationDate());
const modificationDate = formatDate(pdfDoc.getModificationDate());
if (verbose && title) {
console.error(`Title: ${title}`);
}
// Extract form fields
const formFields: FormFieldInfo[] = [];
let hasFormFields = false;
try {
const form = pdfDoc.getForm();
const fields = form.getFields();
hasFormFields = fields.length > 0;
for (const field of fields) {
const name = field.getName();
const fieldType = field.constructor.name;
// deno-lint-ignore no-explicit-any
const fieldInfo: FormFieldInfo = {
name,
type: "unknown",
};
// Determine field type and extract value
if (fieldType.includes("Text")) {
fieldInfo.type = "text";
try {
// deno-lint-ignore no-explicit-any
fieldInfo.value = (field as any).getText();
} catch { /* field may not support getText */ }
} else if (fieldType.includes("CheckBox")) {
fieldInfo.type = "checkbox";
try {
// deno-lint-ignore no-explicit-any
fieldInfo.value = (field as any).isChecked();
} catch { /* field may not support isChecked */ }
} else if (fieldType.includes("Radio")) {
fieldInfo.type = "radio";
try {
// deno-lint-ignore no-explicit-any
fieldInfo.value = (field as any).getSelected();
// deno-lint-ignore no-explicit-any
fieldInfo.options = (field as any).getOptions();
} catch { /* field may not support these methods */ }
} else if (fieldType.includes("Dropdown") || fieldType.includes("OptionList")) {
fieldInfo.type = "dropdown";
try {
// deno-lint-ignore no-explicit-any
fieldInfo.value = (field as any).getSelected()?.[0];
// deno-lint-ignore no-explicit-any
fieldInfo.options = (field as any).getOptions();
} catch { /* field may not support these methods */ }
} else if (fieldType.includes("Button")) {
fieldInfo.type = "button";
} else if (fieldType.includes("Signature")) {
fieldInfo.type = "signature";
}
// Check if read-only
try {
// deno-lint-ignore no-explicit-any
fieldInfo.readOnly = (field as any).isReadOnly?.();
} catch { /* method may not exist */ }
formFields.push(fieldInfo);
}
} catch {
// No form or form access failed
if (verbose) {
console.error("No form fields or form access failed");
}
}
if (verbose) {
console.error(`Form fields: ${formFields.length}`);
}
// Extract text using unpdf
const pages: PageInfo[] = [];
const allPlaceholders: PlaceholderInfo[] = [];
try {
const pdf = await getDocumentProxy(new Uint8Array(data));
for (let i = 1; i <= pageCount; i++) {
if (pageFilter && i !== pageFilter) continue;
// Get page dimensions from pdf-lib
const pdfPage = pdfDoc.getPage(i - 1);
const { width, height } = pdfPage.getSize();
const rotation = pdfPage.getRotation().angle;
// Extract text for this page
const { text } = await extractText(pdf, { mergePages: false });
const pageText = Array.isArray(text) ? text[i - 1] || "" : (i === 1 ? text : "");
const pageInfo: PageInfo = {
pageNumber: i,
width,
height,
text: pageText,
rotation,
};
pages.push(pageInfo);
// Find placeholders in text
const placeholders = findPlaceholders(pageText, i);
allPlaceholders.push(...placeholders);
if (verbose) {
console.error(`Page ${i}: ${pageText.length} chars, ${placeholders.length} placeholders`);
}
}
} catch (error) {
if (verbose) {
console.error(`Text extraction failed: ${error}`);
}
// Fall back to just page info without text
for (let i = 1; i <= pageCount; i++) {
if (pageFilter && i !== pageFilter) continue;
const pdfPage = pdfDoc.getPage(i - 1);
const { width, height } = pdfPage.getSize();
const rotation = pdfPage.getRotation().angle;
pages.push({
pageNumber: i,
width,
height,
text: "",
rotation,
});
}
}
if (verbose) {
console.error(`Total placeholders: ${allPlaceholders.length}`);
}
return {
filename,
pageCount,
title,
author,
subject,
creator,
producer,
creationDate,
modificationDate,
pages,
formFields,
placeholders: allPlaceholders,
hasFormFields,
isEncrypted,
};
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "verbose", "pretty"],
string: ["page"],
alias: { help: "h", verbose: "v" },
default: { verbose: false, pretty: false },
}) as unknown as ParsedArgs;
// Convert page to number if provided
if (parsed.page) {
parsed.page = parseInt(String(parsed.page), 10);
}
if (parsed.help) {
printHelp();
Deno.exit(0);
}
const positionalArgs = parsed._.map(String);
if (positionalArgs.length === 0) {
console.error("Error: No input file provided\n");
printHelp();
Deno.exit(1);
}
const inputPath = positionalArgs[0];
try {
const inventory = await analyzePDF(inputPath, {
verbose: parsed.verbose,
pageFilter: parsed.page,
});
const output = parsed.pretty
? JSON.stringify(inventory, null, 2)
: JSON.stringify(inventory);
console.log(output);
} catch (error) {
console.error(
"Error:",
error instanceof Error ? error.message : String(error)
);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read --allow-write
/**
* generate-from-template.ts - Generate PDF from existing templates
*
* Modifies existing PDF templates by filling form fields, adding content
* overlays, and merging documents. Preserves original formatting.
*
* Usage:
* deno run --allow-read --allow-write scripts/generate-from-template.ts <template.pdf> <spec.json> <output.pdf>
*
* Options:
* -h, --help Show help
* -v, --verbose Enable verbose output
*
* Permissions:
* --allow-read: Read template, specification files, and images
* --allow-write: Write output PDF file
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { basename, dirname, resolve } from "jsr:@std/path@1.0.8";
import {
PDFDocument,
StandardFonts,
rgb,
degrees,
// deno-lint-ignore no-explicit-any
} from "npm:pdf-lib@1.17.1" as any;
// === Types ===
export interface FormFieldValue {
/** Field name as it appears in the PDF form */
name: string;
/** Value to set */
value: string | boolean;
}
export interface TextOverlay {
type: "text";
/** Page number (1-indexed) */
page: number;
x: number;
y: number;
text: string;
fontSize?: number;
font?: "Helvetica" | "HelveticaBold" | "TimesRoman" | "Courier";
color?: { r: number; g: number; b: number };
rotate?: number;
}
export interface ImageOverlay {
type: "image";
/** Page number (1-indexed) */
page: number;
x: number;
y: number;
path: string;
width?: number;
height?: number;
opacity?: number;
}
export interface RectangleOverlay {
type: "rectangle";
page: number;
x: number;
y: number;
width: number;
height: number;
color?: { r: number; g: number; b: number };
opacity?: number;
}
export type Overlay = TextOverlay | ImageOverlay | RectangleOverlay;
export interface MergeSource {
/** Path to PDF file to merge */
path: string;
/** Which pages to include (1-indexed, all if omitted) */
pages?: number[];
}
export interface TemplateSpec {
/** Form field values to fill */
formFields?: FormFieldValue[];
/** Content overlays to add on pages */
overlays?: Overlay[];
/** PDFs to append after the template */
appendPdfs?: MergeSource[];
/** PDFs to prepend before the template */
prependPdfs?: MergeSource[];
/** Flatten form fields (make them non-editable) */
flattenForm?: boolean;
/** Pages to include from template (1-indexed, all if omitted) */
includePages?: number[];
/** Pages to exclude from template (1-indexed) */
excludePages?: number[];
}
interface ParsedArgs {
help: boolean;
verbose: boolean;
_: (string | number)[];
}
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "generate-from-template";
const FONT_MAP: Record<string, typeof StandardFonts[keyof typeof StandardFonts]> = {
"Helvetica": StandardFonts.Helvetica,
"HelveticaBold": StandardFonts.HelveticaBold,
"TimesRoman": StandardFonts.TimesRoman,
"Courier": StandardFonts.Courier,
};
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Generate PDF from existing templates
Usage:
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts <template.pdf> <spec.json> <output.pdf>
Arguments:
<template.pdf> Path to the template PDF file
<spec.json> Path to JSON specification for modifications
<output.pdf> Path for output PDF file
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
Specification Format:
{
"formFields": [
{ "name": "FullName", "value": "John Smith" },
{ "name": "Email", "value": "john@example.com" },
{ "name": "Agree", "value": true }
],
"overlays": [
{
"type": "text",
"page": 1,
"x": 400,
"y": 50,
"text": "APPROVED",
"fontSize": 24,
"color": { "r": 0, "g": 0.5, "b": 0 },
"rotate": 45
}
],
"flattenForm": true
}
Features:
- Fill form fields (text, checkbox, radio, dropdown)
- Add text/image overlays on any page
- Merge multiple PDFs
- Include/exclude specific pages
- Flatten forms to prevent editing
Examples:
# Fill form and save
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
form-template.pdf form-data.json filled-form.pdf
# Add watermark overlay
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
document.pdf watermark-spec.json watermarked.pdf -v
`);
}
// === Utility Functions ===
function getColor(color?: { r: number; g: number; b: number }): ReturnType<typeof rgb> {
if (!color) return rgb(0, 0, 0);
return rgb(color.r, color.g, color.b);
}
// === Core Logic ===
// deno-lint-ignore no-explicit-any
async function loadAndCopyPages(pdfDoc: any, source: MergeSource, specDir: string): Promise<any[]> {
const sourcePath = resolve(specDir, source.path);
const sourceData = await Deno.readFile(sourcePath);
const sourcePdf = await PDFDocument.load(sourceData);
const pageIndices = source.pages
? source.pages.map(p => p - 1) // Convert to 0-indexed
: sourcePdf.getPageIndices();
return await pdfDoc.copyPages(sourcePdf, pageIndices);
}
export async function generateFromTemplate(
templatePath: string,
spec: TemplateSpec,
outputPath: string,
options: { verbose?: boolean; specDir?: string } = {}
): Promise<void> {
const { verbose = false, specDir = "." } = options;
// Load template
const templateData = await Deno.readFile(templatePath);
const templatePdf = await PDFDocument.load(templateData);
if (verbose) {
console.error(`Loaded template: ${basename(templatePath)}`);
console.error(`Template pages: ${templatePdf.getPageCount()}`);
}
// Create output document
const pdfDoc = await PDFDocument.create();
// Copy metadata from template
pdfDoc.setTitle(templatePdf.getTitle() || "");
pdfDoc.setAuthor(templatePdf.getAuthor() || "");
pdfDoc.setSubject(templatePdf.getSubject() || "");
// Embed fonts for overlays
// deno-lint-ignore no-explicit-any
const fonts = new Map<string, any>();
for (const [name, fontEnum] of Object.entries(FONT_MAP)) {
fonts.set(name, await pdfDoc.embedFont(fontEnum));
}
// Add prepended PDFs
if (spec.prependPdfs && spec.prependPdfs.length > 0) {
for (const source of spec.prependPdfs) {
const pages = await loadAndCopyPages(pdfDoc, source, specDir);
for (const page of pages) {
pdfDoc.addPage(page);
}
if (verbose) {
console.error(`Prepended ${pages.length} pages from ${source.path}`);
}
}
}
// Determine which template pages to include
const templatePageCount = templatePdf.getPageCount();
let templatePageIndices: number[] = [];
if (spec.includePages && spec.includePages.length > 0) {
templatePageIndices = spec.includePages.map(p => p - 1);
} else if (spec.excludePages && spec.excludePages.length > 0) {
const excludeSet = new Set(spec.excludePages.map(p => p - 1));
for (let i = 0; i < templatePageCount; i++) {
if (!excludeSet.has(i)) {
templatePageIndices.push(i);
}
}
} else {
templatePageIndices = templatePdf.getPageIndices();
}
// Copy template pages
const templatePages = await pdfDoc.copyPages(templatePdf, templatePageIndices);
// deno-lint-ignore no-explicit-any
const addedPages: any[] = [];
for (const page of templatePages) {
pdfDoc.addPage(page);
addedPages.push(page);
}
if (verbose) {
console.error(`Added ${addedPages.length} template pages`);
}
// Fill form fields
if (spec.formFields && spec.formFields.length > 0) {
try {
const form = pdfDoc.getForm();
let filledCount = 0;
for (const fieldSpec of spec.formFields) {
try {
const field = form.getField(fieldSpec.name);
const fieldType = field.constructor.name;
if (typeof fieldSpec.value === "boolean") {
// Checkbox
if (fieldType.includes("CheckBox")) {
if (fieldSpec.value) {
// deno-lint-ignore no-explicit-any
(field as any).check();
} else {
// deno-lint-ignore no-explicit-any
(field as any).uncheck();
}
filledCount++;
}
} else {
// Text field, dropdown, etc.
if (fieldType.includes("Text")) {
// deno-lint-ignore no-explicit-any
(field as any).setText(fieldSpec.value);
filledCount++;
} else if (fieldType.includes("Dropdown") || fieldType.includes("OptionList")) {
// deno-lint-ignore no-explicit-any
(field as any).select(fieldSpec.value);
filledCount++;
} else if (fieldType.includes("Radio")) {
// deno-lint-ignore no-explicit-any
(field as any).select(fieldSpec.value);
filledCount++;
}
}
} catch (error) {
if (verbose) {
console.error(`Failed to fill field "${fieldSpec.name}": ${error}`);
}
}
}
if (verbose) {
console.error(`Filled ${filledCount} form fields`);
}
// Flatten form if requested
if (spec.flattenForm) {
form.flatten();
if (verbose) {
console.error("Form flattened");
}
}
} catch (error) {
if (verbose) {
console.error(`Form processing failed: ${error}`);
}
}
}
// Apply overlays
if (spec.overlays && spec.overlays.length > 0) {
// Calculate page offset from prepended PDFs
const prependedPageCount = spec.prependPdfs?.reduce((sum, s) => {
// This is approximate - we'd need to track actual pages added
return sum + (s.pages?.length || 0);
}, 0) || 0;
for (const overlay of spec.overlays) {
// Adjust page index for prepended pages and 1-indexing
const pageIndex = (overlay.page - 1) + prependedPageCount;
if (pageIndex < 0 || pageIndex >= pdfDoc.getPageCount()) {
if (verbose) {
console.error(`Overlay page ${overlay.page} out of range`);
}
continue;
}
const page = pdfDoc.getPage(pageIndex);
try {
switch (overlay.type) {
case "text": {
const fontName = overlay.font || "Helvetica";
const font = fonts.get(fontName);
// deno-lint-ignore no-explicit-any
const textOptions: any = {
x: overlay.x,
y: overlay.y,
size: overlay.fontSize || 12,
font,
color: getColor(overlay.color),
};
if (overlay.rotate) {
textOptions.rotate = degrees(overlay.rotate);
}
page.drawText(overlay.text, textOptions);
break;
}
case "image": {
const imagePath = resolve(specDir, overlay.path);
const imageData = await Deno.readFile(imagePath);
const ext = overlay.path.toLowerCase().split(".").pop();
// deno-lint-ignore no-explicit-any
let image: any;
if (ext === "png") {
image = await pdfDoc.embedPng(imageData);
} else if (ext === "jpg" || ext === "jpeg") {
image = await pdfDoc.embedJpg(imageData);
} else {
throw new Error(`Unsupported image format: ${ext}`);
}
const dims = image.scale(1);
page.drawImage(image, {
x: overlay.x,
y: overlay.y,
width: overlay.width || dims.width,
height: overlay.height || dims.height,
opacity: overlay.opacity,
});
break;
}
case "rectangle": {
page.drawRectangle({
x: overlay.x,
y: overlay.y,
width: overlay.width,
height: overlay.height,
color: getColor(overlay.color),
opacity: overlay.opacity,
});
break;
}
}
} catch (error) {
if (verbose) {
console.error(`Failed to apply overlay: ${error}`);
}
}
}
if (verbose) {
console.error(`Applied ${spec.overlays.length} overlays`);
}
}
// Add appended PDFs
if (spec.appendPdfs && spec.appendPdfs.length > 0) {
for (const source of spec.appendPdfs) {
const pages = await loadAndCopyPages(pdfDoc, source, specDir);
for (const page of pages) {
pdfDoc.addPage(page);
}
if (verbose) {
console.error(`Appended ${pages.length} pages from ${source.path}`);
}
}
}
// Write output
const pdfBytes = await pdfDoc.save();
await Deno.writeFile(outputPath, pdfBytes);
if (verbose) {
console.error(`Wrote ${outputPath} (${pdfDoc.getPageCount()} pages)`);
}
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "verbose"],
alias: { help: "h", verbose: "v" },
default: { verbose: false },
}) as ParsedArgs;
if (parsed.help) {
printHelp();
Deno.exit(0);
}
const positionalArgs = parsed._.map(String);
if (positionalArgs.length < 3) {
console.error(
"Error: template.pdf, spec.json, and output.pdf are required\n"
);
printHelp();
Deno.exit(1);
}
const templatePath = positionalArgs[0];
const specPath = positionalArgs[1];
const outputPath = positionalArgs[2];
try {
// Read specification
const specText = await Deno.readTextFile(specPath);
const spec = JSON.parse(specText) as TemplateSpec;
const specDir = dirname(resolve(specPath));
await generateFromTemplate(templatePath, spec, outputPath, {
verbose: parsed.verbose,
specDir,
});
console.log(`Created: ${outputPath}`);
} catch (error) {
console.error(
"Error:",
error instanceof Error ? error.message : String(error)
);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read --allow-write
/**
* generate-scratch.ts - Create PDF from scratch using JSON specification
*
* Creates PDF documents programmatically from a JSON specification
* using pdf-lib. Supports text, images, shapes, and form fields.
*
* Usage:
* deno run --allow-read --allow-write scripts/generate-scratch.ts <spec.json> <output.pdf>
*
* Options:
* -h, --help Show help
* -v, --verbose Enable verbose output
*
* Permissions:
* --allow-read: Read specification file and image assets
* --allow-write: Write output PDF file
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { dirname, resolve } from "jsr:@std/path@1.0.8";
import {
PDFDocument,
StandardFonts,
rgb,
degrees,
PageSizes,
// deno-lint-ignore no-explicit-any
} from "npm:pdf-lib@1.17.1" as any;
// === Types ===
export interface TextElement {
type: "text";
x: number;
y: number;
text: string;
fontSize?: number;
font?: "Helvetica" | "HelveticaBold" | "HelveticaOblique" | "TimesRoman" | "TimesBold" | "Courier" | "CourierBold";
color?: { r: number; g: number; b: number };
maxWidth?: number;
lineHeight?: number;
rotate?: number;
}
export interface ImageElement {
type: "image";
x: number;
y: number;
path: string;
width?: number;
height?: number;
opacity?: number;
rotate?: number;
}
export interface RectangleElement {
type: "rectangle";
x: number;
y: number;
width: number;
height: number;
color?: { r: number; g: number; b: number };
borderColor?: { r: number; g: number; b: number };
borderWidth?: number;
opacity?: number;
}
export interface LineElement {
type: "line";
startX: number;
startY: number;
endX: number;
endY: number;
color?: { r: number; g: number; b: number };
thickness?: number;
opacity?: number;
}
export interface CircleElement {
type: "circle";
x: number;
y: number;
radius: number;
color?: { r: number; g: number; b: number };
borderColor?: { r: number; g: number; b: number };
borderWidth?: number;
opacity?: number;
}
export interface TableElement {
type: "table";
x: number;
y: number;
rows: string[][];
columnWidths: number[];
rowHeight?: number;
fontSize?: number;
headerBackground?: { r: number; g: number; b: number };
borderColor?: { r: number; g: number; b: number };
padding?: number;
}
export type PageElement = TextElement | ImageElement | RectangleElement | LineElement | CircleElement | TableElement;
export interface PageSpec {
size?: "A4" | "Letter" | "Legal" | [number, number];
margins?: { top?: number; right?: number; bottom?: number; left?: number };
elements: PageElement[];
}
export interface PDFSpec {
title?: string;
author?: string;
subject?: string;
creator?: string;
pages: PageSpec[];
}
interface ParsedArgs {
help: boolean;
verbose: boolean;
_: (string | number)[];
}
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "generate-scratch";
const FONT_MAP: Record<string, typeof StandardFonts[keyof typeof StandardFonts]> = {
"Helvetica": StandardFonts.Helvetica,
"HelveticaBold": StandardFonts.HelveticaBold,
"HelveticaOblique": StandardFonts.HelveticaOblique,
"TimesRoman": StandardFonts.TimesRoman,
"TimesBold": StandardFonts.TimesBold,
"Courier": StandardFonts.Courier,
"CourierBold": StandardFonts.CourierBold,
};
const PAGE_SIZES: Record<string, [number, number]> = {
"A4": PageSizes.A4,
"Letter": PageSizes.Letter,
"Legal": PageSizes.Legal,
};
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Create PDF from scratch using JSON specification
Usage:
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts <spec.json> <output.pdf>
Arguments:
<spec.json> Path to JSON specification file
<output.pdf> Path for output PDF file
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
Specification Format:
{
"title": "My Document",
"author": "Author Name",
"pages": [
{
"size": "A4",
"elements": [
{
"type": "text",
"x": 50,
"y": 750,
"text": "Hello World",
"fontSize": 24,
"font": "HelveticaBold",
"color": { "r": 0, "g": 0, "b": 0.5 }
},
{
"type": "rectangle",
"x": 50,
"y": 700,
"width": 200,
"height": 2,
"color": { "r": 0, "g": 0, "b": 0 }
}
]
}
]
}
Supported Elements:
- text: Text with font, size, color options
- image: PNG/JPEG images from file
- rectangle: Filled or outlined rectangles
- line: Straight lines
- circle: Filled or outlined circles
- table: Basic table layout (manual positioning)
Examples:
# Generate PDF
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts spec.json output.pdf
# With verbose output
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts spec.json output.pdf -v
`);
}
// === Utility Functions ===
function getColor(color?: { r: number; g: number; b: number }): ReturnType<typeof rgb> {
if (!color) return rgb(0, 0, 0);
return rgb(color.r, color.g, color.b);
}
// === Element Drawing Functions ===
// deno-lint-ignore no-explicit-any
async function drawText(page: any, element: TextElement, fonts: Map<string, any>): Promise<void> {
const fontName = element.font || "Helvetica";
const font = fonts.get(fontName);
const fontSize = element.fontSize || 12;
const options: Record<string, unknown> = {
x: element.x,
y: element.y,
size: fontSize,
font,
color: getColor(element.color),
};
if (element.maxWidth) {
options.maxWidth = element.maxWidth;
options.lineHeight = element.lineHeight || fontSize * 1.2;
}
if (element.rotate) {
options.rotate = degrees(element.rotate);
}
page.drawText(element.text, options);
}
// deno-lint-ignore no-explicit-any
async function drawImage(page: any, element: ImageElement, pdfDoc: any, specDir: string): Promise<void> {
const imagePath = resolve(specDir, element.path);
const imageData = await Deno.readFile(imagePath);
const ext = element.path.toLowerCase().split(".").pop();
// deno-lint-ignore no-explicit-any
let image: any;
if (ext === "png") {
image = await pdfDoc.embedPng(imageData);
} else if (ext === "jpg" || ext === "jpeg") {
image = await pdfDoc.embedJpg(imageData);
} else {
throw new Error(`Unsupported image format: ${ext}`);
}
const dims = image.scale(1);
const width = element.width || dims.width;
const height = element.height || dims.height;
const options: Record<string, unknown> = {
x: element.x,
y: element.y,
width,
height,
};
if (element.opacity !== undefined) {
options.opacity = element.opacity;
}
if (element.rotate) {
options.rotate = degrees(element.rotate);
}
page.drawImage(image, options);
}
// deno-lint-ignore no-explicit-any
function drawRectangle(page: any, element: RectangleElement): void {
const options: Record<string, unknown> = {
x: element.x,
y: element.y,
width: element.width,
height: element.height,
};
if (element.color) {
options.color = getColor(element.color);
}
if (element.borderColor) {
options.borderColor = getColor(element.borderColor);
options.borderWidth = element.borderWidth || 1;
}
if (element.opacity !== undefined) {
options.opacity = element.opacity;
}
page.drawRectangle(options);
}
// deno-lint-ignore no-explicit-any
function drawLine(page: any, element: LineElement): void {
const options: Record<string, unknown> = {
start: { x: element.startX, y: element.startY },
end: { x: element.endX, y: element.endY },
color: getColor(element.color),
thickness: element.thickness || 1,
};
if (element.opacity !== undefined) {
options.opacity = element.opacity;
}
page.drawLine(options);
}
// deno-lint-ignore no-explicit-any
function drawCircle(page: any, element: CircleElement): void {
const options: Record<string, unknown> = {
x: element.x,
y: element.y,
size: element.radius,
};
if (element.color) {
options.color = getColor(element.color);
}
if (element.borderColor) {
options.borderColor = getColor(element.borderColor);
options.borderWidth = element.borderWidth || 1;
}
if (element.opacity !== undefined) {
options.opacity = element.opacity;
}
page.drawCircle(options);
}
// deno-lint-ignore no-explicit-any
async function drawTable(page: any, element: TableElement, fonts: Map<string, any>): Promise<void> {
const font = fonts.get("Helvetica");
const fontSize = element.fontSize || 10;
const rowHeight = element.rowHeight || 20;
const padding = element.padding || 5;
const borderColor = getColor(element.borderColor || { r: 0, g: 0, b: 0 });
let currentY = element.y;
for (let rowIndex = 0; rowIndex < element.rows.length; rowIndex++) {
const row = element.rows[rowIndex];
let currentX = element.x;
for (let colIndex = 0; colIndex < row.length; colIndex++) {
const cellWidth = element.columnWidths[colIndex] || 100;
const cellText = row[colIndex];
// Draw cell background for header
if (rowIndex === 0 && element.headerBackground) {
page.drawRectangle({
x: currentX,
y: currentY - rowHeight,
width: cellWidth,
height: rowHeight,
color: getColor(element.headerBackground),
});
}
// Draw cell border
page.drawRectangle({
x: currentX,
y: currentY - rowHeight,
width: cellWidth,
height: rowHeight,
borderColor,
borderWidth: 0.5,
});
// Draw cell text
page.drawText(cellText, {
x: currentX + padding,
y: currentY - rowHeight + padding + 2,
size: fontSize,
font,
color: rgb(0, 0, 0),
});
currentX += cellWidth;
}
currentY -= rowHeight;
}
}
// === Core Logic ===
export async function generateFromSpec(
spec: PDFSpec,
outputPath: string,
options: { verbose?: boolean; specDir?: string } = {}
): Promise<void> {
const { verbose = false, specDir = "." } = options;
// Create document
const pdfDoc = await PDFDocument.create();
// Set metadata
if (spec.title) pdfDoc.setTitle(spec.title);
if (spec.author) pdfDoc.setAuthor(spec.author);
if (spec.subject) pdfDoc.setSubject(spec.subject);
if (spec.creator) pdfDoc.setCreator(spec.creator);
// Embed fonts
// deno-lint-ignore no-explicit-any
const fonts = new Map<string, any>();
for (const [name, fontEnum] of Object.entries(FONT_MAP)) {
fonts.set(name, await pdfDoc.embedFont(fontEnum));
}
if (verbose) {
console.error(`Creating PDF with ${spec.pages.length} page(s)`);
}
// Process each page
for (let pageIndex = 0; pageIndex < spec.pages.length; pageIndex++) {
const pageSpec = spec.pages[pageIndex];
// Determine page size
let pageSize: [number, number] = PageSizes.A4;
if (pageSpec.size) {
if (Array.isArray(pageSpec.size)) {
pageSize = pageSpec.size;
} else if (PAGE_SIZES[pageSpec.size]) {
pageSize = PAGE_SIZES[pageSpec.size];
}
}
const page = pdfDoc.addPage(pageSize);
if (verbose) {
console.error(`Page ${pageIndex + 1}: ${pageSpec.elements.length} elements`);
}
// Draw elements
for (const element of pageSpec.elements) {
try {
switch (element.type) {
case "text":
await drawText(page, element, fonts);
break;
case "image":
await drawImage(page, element, pdfDoc, specDir);
break;
case "rectangle":
drawRectangle(page, element);
break;
case "line":
drawLine(page, element);
break;
case "circle":
drawCircle(page, element);
break;
case "table":
await drawTable(page, element, fonts);
break;
default:
if (verbose) {
console.error(`Unknown element type: ${(element as PageElement).type}`);
}
}
} catch (error) {
if (verbose) {
console.error(`Error drawing element: ${error}`);
}
}
}
}
// Write output
const pdfBytes = await pdfDoc.save();
await Deno.writeFile(outputPath, pdfBytes);
if (verbose) {
console.error(`Wrote ${outputPath}`);
}
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "verbose"],
alias: { help: "h", verbose: "v" },
default: { verbose: false },
}) as ParsedArgs;
if (parsed.help) {
printHelp();
Deno.exit(0);
}
const positionalArgs = parsed._.map(String);
if (positionalArgs.length < 2) {
console.error("Error: Both spec.json and output.pdf are required\n");
printHelp();
Deno.exit(1);
}
const specPath = positionalArgs[0];
const outputPath = positionalArgs[1];
try {
const specText = await Deno.readTextFile(specPath);
const spec = JSON.parse(specText) as PDFSpec;
const specDir = dirname(resolve(specPath));
await generateFromSpec(spec, outputPath, {
verbose: parsed.verbose,
specDir,
});
console.log(`Created: ${outputPath}`);
} catch (error) {
console.error(
"Error:",
error instanceof Error ? error.message : String(error)
);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
Related skills
How it compares
Choose pdf-generator over generic document skills when you need Deno-native PDF create, fill, merge, and watermark operations in one agent workflow.
FAQ
What runtime does pdf-generator require?
pdf-generator requires Deno with --allow-read and --allow-write permissions so the skill can read template PDFs and write generated output files. The skill is version 1.0, MIT-licensed, and tagged as a generator-type agent skill.
Can pdf-generator fill existing PDF forms?
pdf-generator supports template-based generation including form filling and overlays, not only from-scratch PDF creation. Developers can also merge documents, add watermarks, and extract text content from existing PDFs.
Is Pdf Generator safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.