document-processing
PDF/DOCX/XLSX/PPTX generation and parsing on Cloudflare Workers. Covers CF Browser Rendering → PDF, pdf-lib Worker-native generation, docx/exceljs output, pptxgenjs slides, and RAG-ready text extraction. Use cases: donor annual reports, SaaS invoices, tax receipts, financial repo
Install
npx skills add https://github.com/heymegabyte/claude-skills/tree/master/18-document-processing
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install heymegabyte-claude-skills@llmmart
git clone https://github.com/heymegabyte/claude-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole heymegabyte/claude-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
18 — Document Processing
Worker-native document I/O. All generation runs at the edge — no Lambda, no container, no third-party conversion SaaS.
Sub-modules
pdf-generation.md— CF Browser Rendering → PDF + pdf-lib fallbackpdf-parsing.md— text + table extraction for RAG ingestiondocx-xlsx.md— DOCX (docx library) + XLSX (exceljs) generation in Workerspptx-generation.md— PPTX via pptxgenjs in Workers
Decision tree
Need document output?
├── PDF (invoice / receipt / report)
│ ├── Complex layout (HTML → PDF) → CF Browser Rendering
│ └── Programmatic (no layout) → pdf-lib in Worker
├── DOCX / XLSX (data export / mail merge)
│ ├── DOCX → docx library (pure JS, Worker-compat)
│ └── XLSX → exceljs (no canvas dep, Worker-compat)
└── PPTX (slide deck / pitch deck)
└── pptxgenjs (Worker-compat, no native deps)
Need document input (RAG)?
├── PDF text → pdf-parse (pure JS) or Workers AI document extraction
└── Tables → structured JSON → D1 or Vectorize
Cloudflare primitives used
CF Browser Rendering— puppeteer-compatible Workers binding for HTML → PDFR2— store and serve generated documentsD1— job state + document metadataWorkers AI— optional OCR for scanned PDFs (Llama Vision)Queues— async generation jobs (large reports)
Use case map
| Use case | Format | Method |
|---|---|---|
| SaaS invoice | pdf-lib → R2 | |
| Tax receipt (nonprofit) | CF Browser Rendering → R2 | |
| Donor annual report | CF Browser Rendering (full layout) | |
| Financial export | XLSX | exceljs → R2 |
| Grant application | DOCX | docx → R2 |
| Board slide deck | PPTX | pptxgenjs → R2 |
| RAG: donor docs | Text | pdf-parse → Vectorize |
Cross-links
rules/cloudflare-lock-in-is-leverage.md— CF Browser Rendering over puppeteer SaaSrules/feature-flags.md— gate new doc types behind flag before GA13-observability-and-growth/— track document generation events in PostHog08-deploy-and-runtime-verification/— smoke-test R2 presigned URL after deploy
Files (claude-skills)
-
docx-xlsx.md 7.4 KB
# DOCX + XLSX Generation on Cloudflare Workers Source: anthropics/skills docx/xlsx patterns Both `docx` and `exceljs` are pure-JS, Worker-compatible. No native dependencies. --- ## DOCX Generation (docx library) Best for: grant applications, donor letters, mail merge, contracts, proposals. ### Install ```bash npm install docx ``` ### Worker-compatible check `docx` uses no Node built-ins — pure JS, zero native deps, Worker-compatible out of the box. ### Basic DOCX generation ```typescript import { Document, Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, BorderStyle, Packer, } from "docx"; export interface GrantApplicationData { orgName: string; programName: string; requestAmount: number; narrative: string; budget: Array<{ category: string; amount: number; description: string }>; contactName: string; contactEmail: string; } export async function generateGrantApplication( env: Env, data: GrantApplicationData, ): Promise<string> { const doc = new Document({ styles: { default: { heading1: { run: { font: "Calibri", size: 32, bold: true, color: "060610" }, }, }, }, sections: [ { children: [ // Title new Paragraph({ text: data.orgName, heading: HeadingLevel.TITLE, }), new Paragraph({ text: `Grant Application — ${data.programName}`, heading: HeadingLevel.HEADING_1, }), new Paragraph({ text: "" }), // Request amount new Paragraph({ children: [ new TextRun({ text: "Requested Amount: ", bold: true }), new TextRun({ text: `$${data.requestAmount.toLocaleString()}` }), ], }), new Paragraph({ text: "" }), // Narrative new Paragraph({ text: "Program Narrative", heading: HeadingLevel.HEADING_2 }), new Paragraph({ text: data.narrative }), new Paragraph({ text: "" }), // Budget table new Paragraph({ text: "Budget Summary", heading: HeadingLevel.HEADING_2 }), new Table({ rows: [ new TableRow({ children: [ new TableCell({ children: [new Paragraph({ text: "Category", run: { bold: true } } as any)] }), new TableCell({ children: [new Paragraph({ text: "Amount", run: { bold: true } } as any)] }), new TableCell({ children: [new Paragraph({ text: "Description", run: { bold: true } } as any)] }), ], }), ...data.budget.map( (line) => new TableRow({ children: [ new TableCell({ children: [new Paragraph(line.category)] }), new TableCell({ children: [new Paragraph(`$${line.amount.toLocaleString()}`)] }), new TableCell({ children: [new Paragraph(line.description)] }), ], }), ), ], }), new Paragraph({ text: "" }), // Contact new Paragraph({ text: "Contact Information", heading: HeadingLevel.HEADING_2 }), new Paragraph({ text: `${data.contactName} — ${data.contactEmail}` }), ], }, ], }); const buffer = await Packer.toBuffer(doc); const key = `documents/grants/${data.orgName.replace(/\s+/g, "-")}-application.docx`; await env.R2.put(key, buffer, { httpMetadata: { contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", }, }); return env.R2.createPresignedUrl(key, { expiresIn: 3600 }); } ``` --- ## XLSX Generation (exceljs) Best for: financial reports, donor lists, data exports, pivot-ready datasets. ### Install ```bash npm install exceljs ``` ### Worker compatibility `exceljs` has no native deps and works in Workers with `nodejs_compat` flag. Avoid `fs` module — use Buffer/ArrayBuffer directly. ### Financial report generation ```typescript import ExcelJS from "exceljs"; export interface FinancialReportData { orgName: string; period: string; revenue: Array<{ category: string; amount: number }>; expenses: Array<{ category: string; amount: number }>; } export async function generateFinancialReport( env: Env, data: FinancialReportData, ): Promise<string> { const workbook = new ExcelJS.Workbook(); workbook.creator = data.orgName; workbook.created = new Date(); // ---- Sheet 1: Summary ---- const summary = workbook.addWorksheet("Summary"); summary.columns = [ { header: "Category", key: "category", width: 30 }, { header: "Amount", key: "amount", width: 15 }, ]; // Style header row const headerRow = summary.getRow(1); headerRow.font = { bold: true, color: { argb: "FFFFFFFF" } }; headerRow.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF060610" } }; // Revenue section summary.addRow({ category: `Revenue — ${data.period}`, amount: "" }).font = { bold: true }; let totalRevenue = 0; for (const line of data.revenue) { summary.addRow({ category: ` ${line.category}`, amount: line.amount }); totalRevenue += line.amount; } summary.addRow({ category: "Total Revenue", amount: totalRevenue }).font = { bold: true }; summary.addRow({}); // Expense section summary.addRow({ category: "Expenses", amount: "" }).font = { bold: true }; let totalExpenses = 0; for (const line of data.expenses) { summary.addRow({ category: ` ${line.category}`, amount: line.amount }); totalExpenses += line.amount; } summary.addRow({ category: "Total Expenses", amount: totalExpenses }).font = { bold: true }; summary.addRow({}); // Net const net = totalRevenue - totalExpenses; const netRow = summary.addRow({ category: "Net Income / (Deficit)", amount: net }); netRow.font = { bold: true, color: { argb: net >= 0 ? "FF00A86B" : "FFDC143C" } }; // Format currency column summary.getColumn("amount").numFmt = '"$"#,##0.00;[Red]-"$"#,##0.00'; // ---- Sheet 2: Raw data ---- const raw = workbook.addWorksheet("Detail"); raw.addRow(["Type", "Category", "Amount"]); for (const r of data.revenue) raw.addRow(["Revenue", r.category, r.amount]); for (const e of data.expenses) raw.addRow(["Expense", e.category, e.amount]); // Export to buffer const buffer = await workbook.xlsx.writeBuffer(); const key = `documents/reports/${data.orgName.replace(/\s+/g, "-")}-${data.period}.xlsx`; await env.R2.put(key, buffer, { httpMetadata: { contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }, }); return env.R2.createPresignedUrl(key, { expiresIn: 86400 }); } ``` --- ## Route handlers ```typescript app.post("/api/documents/grant", async (c) => { const data = GrantApplicationSchema.parse(await c.req.json()); const url = await generateGrantApplication(c.env, data); return c.json({ url }); }); app.post("/api/documents/financial-report", async (c) => { const data = FinancialReportSchema.parse(await c.req.json()); const url = await generateFinancialReport(c.env, data); return c.json({ url }); }); ``` ## Worker compatibility notes - Add `compatibility_flags = ["nodejs_compat"]` to `wrangler.toml` - `exceljs` uses `Buffer` — covered by `nodejs_compat` - Both libraries write to in-memory buffers — no `fs.writeFile` calls needed - Output always routes to R2 + presigned URL — never stream raw binary in response -
pdf-generation.md 5.1 KB
# PDF Generation on Cloudflare Workers Source: anthropics/skills pdf pattern + CF Browser Rendering docs Two methods. Choose based on layout complexity. --- ## Method A: CF Browser Rendering → PDF (complex layouts) Best for: annual reports, branded invoices, full-layout donor receipts. ### Worker setup (wrangler.toml) ```toml [[browser]] binding = "BROWSER" ``` ### Worker code ```typescript import { launch } from "@cloudflare/puppeteer"; export interface Env { BROWSER: Fetcher; R2: R2Bucket; } export async function generatePDF( env: Env, html: string, filename: string ): Promise<string> { const browser = await launch(env.BROWSER); const page = await browser.newPage(); await page.setContent(html, { waitUntil: "networkidle0" }); await page.emulateMediaType("print"); const pdf = await page.pdf({ format: "Letter", printBackground: true, margin: { top: "0.75in", right: "0.75in", bottom: "0.75in", left: "0.75in" }, }); await browser.close(); // Store in R2 const key = `documents/${filename}`; await env.R2.put(key, pdf, { httpMetadata: { contentType: "application/pdf" }, }); // Return presigned URL (1-hour TTL) const url = await env.R2.createPresignedUrl(key, { expiresIn: 3600 }); return url; } ``` ### HTML template pattern (for branded PDFs) ```typescript function invoiceHTML(data: InvoiceData): string { return `<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> @import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700&display=swap'); * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Sora', sans-serif; color: #060610; font-size: 14px; line-height: 1.6; } .header { background: #060610; color: #00E5FF; padding: 2rem; } .logo { font-size: 1.5rem; font-weight: 700; } .body { padding: 2rem; } table { width: 100%; border-collapse: collapse; margin: 1rem 0; } th { background: #f5f5f7; text-align: left; padding: 0.5rem; } td { padding: 0.5rem; border-bottom: 1px solid #e5e5e5; } .total { font-weight: 700; font-size: 1.1rem; } </style> </head> <body> <div class="header"> <div class="logo">${data.companyName}</div> <div>Invoice #${data.invoiceNumber}</div> </div> <div class="body"> <p>Bill to: ${data.customerName}</p> <p>Date: ${data.date} | Due: ${data.dueDate}</p> <table> <tr><th>Description</th><th>Qty</th><th>Rate</th><th>Amount</th></tr> ${data.lineItems.map(item => ` <tr> <td>${item.description}</td> <td>${item.quantity}</td> <td>$${item.rate.toFixed(2)}</td> <td>$${(item.quantity * item.rate).toFixed(2)}</td> </tr> `).join("")} <tr class="total"><td colspan="3">Total</td><td>$${data.total.toFixed(2)}</td></tr> </table> </div> </body> </html>`; } ``` ### Route handler ```typescript app.post("/api/documents/invoice", async (c) => { const data = InvoiceSchema.parse(await c.req.json()); const html = invoiceHTML(data); const url = await generatePDF(c.env, html, `invoice-${data.invoiceNumber}.pdf`); return c.json({ url, expiresIn: 3600 }); }); ``` --- ## Method B: pdf-lib (programmatic, no layout) Best for: tax receipts, simple certificates, data exports. ### Install ```bash npm install pdf-lib ``` ### Worker code ```typescript import { PDFDocument, rgb, StandardFonts } from "pdf-lib"; export async function generateReceiptPDF( env: Env, receipt: TaxReceiptData, ): Promise<string> { const doc = await PDFDocument.create(); const page = doc.addPage([612, 792]); // Letter size in points const font = await doc.embedFont(StandardFonts.Helvetica); const bold = await doc.embedFont(StandardFonts.HelveticaBold); const { height } = page.getSize(); let y = height - 72; // Header page.drawText(receipt.orgName, { x: 72, y, font: bold, size: 20, color: rgb(0, 0.9, 1) }); y -= 30; page.drawText("Official Tax Receipt", { x: 72, y, font, size: 14, color: rgb(0.4, 0.4, 0.4) }); y -= 50; // Body const lines = [ `Receipt #: ${receipt.receiptNumber}`, `Date: ${receipt.date}`, `Donor: ${receipt.donorName}`, `Amount: $${receipt.amount.toFixed(2)}`, `EIN: ${receipt.ein}`, "", "No goods or services were provided in exchange for this contribution.", "This receipt is your official record for tax purposes.", ]; for (const line of lines) { page.drawText(line, { x: 72, y, font: line.startsWith("Amount") ? bold : font, size: 12 }); y -= 20; } const pdfBytes = await doc.save(); const key = `receipts/${receipt.receiptNumber}.pdf`; await env.R2.put(key, pdfBytes, { httpMetadata: { contentType: "application/pdf" } }); return env.R2.createPresignedUrl(key, { expiresIn: 86400 }); // 24h for receipts } ``` --- ## Worker compatibility notes - `pdf-lib` is pure JS — no Node built-ins, Worker-compatible out of the box - `@cloudflare/puppeteer` requires `BROWSER` binding (available on paid plans) - R2 presigned URLs require `R2Object.createPresignedUrl` — available in Workers runtime ≥2024-01 - Never return raw PDF bytes in the response body — always R2 + presigned URL -
pdf-parsing.md 5.6 KB
# PDF Parsing on Cloudflare Workers Source: anthropics/skills pdf parsing pattern Extract text and tables from uploaded PDFs for RAG ingestion, search indexing, or structured data extraction. --- ## Method A: pdf-parse (pure JS, Worker-compatible) Best for: text extraction from digital PDFs (not scanned images). ### Install ```bash npm install pdf-parse ``` ### Worker code ```typescript import pdfParse from "pdf-parse"; export interface ParsedPDF { text: string; pages: number; metadata: Record<string, string>; chunks: TextChunk[]; } export interface TextChunk { page: number; text: string; tokens: number; // estimated (chars / 4) } export async function parsePDF(buffer: ArrayBuffer): Promise<ParsedPDF> { const data = await pdfParse(Buffer.from(buffer)); const rawText = data.text; const pages = data.numpages; // Split into chunks for RAG (max ~500 tokens each) const chunks = chunkText(rawText, pages, 2000); // 2000 chars ≈ 500 tokens return { text: rawText, pages, metadata: data.info ?? {}, chunks, }; } function chunkText(text: string, totalPages: number, maxChars: number): TextChunk[] { const chunks: TextChunk[] = []; const paragraphs = text.split(/\n\n+/); let current = ""; let pageEstimate = 1; for (const para of paragraphs) { if ((current + para).length > maxChars && current.length > 0) { chunks.push({ page: pageEstimate, text: current.trim(), tokens: Math.ceil(current.length / 4), }); current = ""; pageEstimate = Math.min(pageEstimate + 1, totalPages); } current += para + "\n\n"; } if (current.trim()) { chunks.push({ page: pageEstimate, text: current.trim(), tokens: Math.ceil(current.length / 4) }); } return chunks; } ``` --- ## Method B: Workers AI Llama Vision (scanned PDFs / images) Best for: scanned documents, forms, handwritten content. ```typescript export async function parsePDFWithVision( env: Env, imageBuffer: ArrayBuffer, // Convert PDF page to image first ): Promise<string> { const response = await env.AI.run("@cf/meta/llama-3.2-11b-vision-instruct", { messages: [ { role: "user", content: [ { type: "image", image: [...new Uint8Array(imageBuffer)] }, { type: "text", text: "Extract all text from this document. Preserve structure. Output plain text only." }, ], }, ], max_tokens: 2048, }); return (response as { response: string }).response; } ``` --- ## RAG ingestion pipeline Full pipeline: upload → parse → chunk → embed → store in Vectorize. ```typescript import { Vectorize } from "@cloudflare/workers-types"; export interface Env { R2: R2Bucket; VECTORIZE: VectorizeIndex; AI: Ai; DB: D1Database; } export async function ingestDocument( env: Env, file: File, documentId: string, metadata: { title: string; source: string; userId: string }, ): Promise<{ chunks: number; vectors: number }> { // 1. Parse const buffer = await file.arrayBuffer(); const parsed = await parsePDF(buffer); // 2. Store original in R2 await env.R2.put(`documents/${documentId}/original.pdf`, buffer, { httpMetadata: { contentType: "application/pdf" }, customMetadata: metadata, }); // 3. Embed each chunk const vectors: VectorizeVector[] = []; for (const [i, chunk] of parsed.chunks.entries()) { const embedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: chunk.text, }); vectors.push({ id: `${documentId}-chunk-${i}`, values: (embedding as { data: number[][] }).data[0], metadata: { documentId, chunkIndex: i, page: chunk.page, text: chunk.text.slice(0, 500), // Store preview in metadata ...metadata, }, }); } // 4. Upsert into Vectorize await env.VECTORIZE.upsert(vectors); // 5. Record in D1 await env.DB.prepare( `INSERT OR REPLACE INTO documents (id, title, source, user_id, chunks, pages, ingested_at) VALUES (?, ?, ?, ?, ?, ?, ?)` ) .bind(documentId, metadata.title, metadata.source, metadata.userId, parsed.chunks.length, parsed.pages, new Date().toISOString()) .run(); return { chunks: parsed.chunks.length, vectors: vectors.length }; } ``` ### D1 schema ```sql CREATE TABLE documents ( id TEXT PRIMARY KEY, title TEXT NOT NULL, source TEXT, user_id TEXT NOT NULL, chunks INTEGER NOT NULL DEFAULT 0, pages INTEGER NOT NULL DEFAULT 0, ingested_at TEXT NOT NULL, INDEX idx_documents_user (user_id) ); ``` ### Query (RAG retrieval) ```typescript export async function queryDocuments( env: Env, query: string, userId: string, topK = 5, ): Promise<Array<{ text: string; score: number; documentId: string; page: number }>> { // Embed query const embedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: query }); const queryVector = (embedding as { data: number[][] }).data[0]; // Search Vectorize const results = await env.VECTORIZE.query(queryVector, { topK, filter: { userId }, returnMetadata: "all", }); return results.matches.map((m) => ({ text: (m.metadata?.text as string) ?? "", score: m.score, documentId: (m.metadata?.documentId as string) ?? "", page: (m.metadata?.page as number) ?? 0, })); } ``` --- ## Worker compatibility notes - `pdf-parse` depends on `Buffer` — available in Workers via CF compatibility flags - Add `compatibility_flags = ["nodejs_compat"]` to `wrangler.toml` - Large PDFs (>10MB): upload to R2 first via presigned PUT, then process async via Queue + Workflow - Vectorize limit: 1536 dimensions (bge-base-en-v1.5 = 768 dims — ✓ compatible) -
pptx-generation.md 6 KB
# PPTX Generation on Cloudflare Workers Source: anthropics/skills pptx pattern + pptxgenjs docs Generate slide decks programmatically: pitch decks, board updates, donor presentations, training materials. --- ## Install ```bash npm install pptxgenjs ``` `pptxgenjs` is pure JS, no native deps. Worker-compatible with `nodejs_compat` flag. --- ## Basic slide deck ```typescript import PptxGenJS from "pptxgenjs"; export interface PitchDeckData { companyName: string; tagline: string; problem: string; solution: string; marketSize: string; traction: Array<{ metric: string; value: string }>; team: Array<{ name: string; title: string }>; ask: string; } export async function generatePitchDeck( env: Env, data: PitchDeckData, ): Promise<string> { const pptx = new PptxGenJS(); // Theme pptx.layout = "LAYOUT_WIDE"; // 16:9 pptx.theme = { headFontFace: "Calibri", bodyFontFace: "Calibri" }; // ---- Slide 1: Title ---- const titleSlide = pptx.addSlide(); titleSlide.background = { color: "060610" }; titleSlide.addText(data.companyName, { x: 0.5, y: 1.5, w: "90%", h: 1.5, fontSize: 54, bold: true, color: "00E5FF", align: "center", }); titleSlide.addText(data.tagline, { x: 0.5, y: 3.2, w: "90%", h: 0.8, fontSize: 24, color: "CCCCCC", align: "center", }); // ---- Slide 2: Problem ---- const problemSlide = pptx.addSlide(); problemSlide.background = { color: "060610" }; addSectionHeader(pptx, problemSlide, "The Problem"); problemSlide.addText(data.problem, { x: 0.5, y: 2.0, w: "90%", h: 3.0, fontSize: 20, color: "FFFFFF", wrap: true, valign: "top", }); // ---- Slide 3: Solution ---- const solutionSlide = pptx.addSlide(); solutionSlide.background = { color: "060610" }; addSectionHeader(pptx, solutionSlide, "Our Solution"); solutionSlide.addText(data.solution, { x: 0.5, y: 2.0, w: "90%", h: 3.0, fontSize: 20, color: "FFFFFF", wrap: true, valign: "top", }); // ---- Slide 4: Market Size ---- const marketSlide = pptx.addSlide(); marketSlide.background = { color: "060610" }; addSectionHeader(pptx, marketSlide, "Market Opportunity"); marketSlide.addText(data.marketSize, { x: 0.5, y: 2.0, w: "90%", h: 3.0, fontSize: 20, color: "FFFFFF", wrap: true, }); // ---- Slide 5: Traction ---- const tractionSlide = pptx.addSlide(); tractionSlide.background = { color: "060610" }; addSectionHeader(pptx, tractionSlide, "Traction"); data.traction.forEach((item, i) => { const col = i % 3; const row = Math.floor(i / 3); const x = 0.5 + col * 4.0; const y = 2.0 + row * 2.2; tractionSlide.addText(item.value, { x, y, w: 3.5, h: 1.0, fontSize: 36, bold: true, color: "00E5FF", align: "center", }); tractionSlide.addText(item.metric, { x, y: y + 1.0, w: 3.5, h: 0.5, fontSize: 14, color: "AAAAAA", align: "center", }); }); // ---- Slide 6: Team ---- const teamSlide = pptx.addSlide(); teamSlide.background = { color: "060610" }; addSectionHeader(pptx, teamSlide, "Team"); data.team.forEach((member, i) => { const x = 0.5 + i * 4.0; teamSlide.addText(member.name, { x, y: 2.2, w: 3.5, h: 0.7, fontSize: 20, bold: true, color: "FFFFFF", align: "center", }); teamSlide.addText(member.title, { x, y: 2.9, w: 3.5, h: 0.5, fontSize: 14, color: "7C3AED", align: "center", }); }); // ---- Slide 7: The Ask ---- const askSlide = pptx.addSlide(); askSlide.background = { color: "060610" }; addSectionHeader(pptx, askSlide, "The Ask"); askSlide.addText(data.ask, { x: 0.5, y: 2.0, w: "90%", h: 3.0, fontSize: 24, color: "00E5FF", align: "center", bold: true, wrap: true, }); // Export const buffer = await pptx.write({ outputType: "arraybuffer" }) as ArrayBuffer; const key = `documents/decks/${data.companyName.replace(/\s+/g, "-")}-pitch.pptx`; await env.R2.put(key, buffer, { httpMetadata: { contentType: "application/vnd.openxmlformats-officedocument.presentationml.presentation", }, }); return env.R2.createPresignedUrl(key, { expiresIn: 3600 }); } // Helper: section header style function addSectionHeader(pptx: PptxGenJS, slide: PptxGenJS.Slide, title: string): void { slide.addShape(pptx.ShapeType.rect, { x: 0, y: 0, w: "100%", h: 1.4, fill: { color: "0D0D1A" }, line: { color: "00E5FF", width: 0 }, }); slide.addText(title, { x: 0.5, y: 0.2, w: "90%", h: 1.0, fontSize: 32, bold: true, color: "00E5FF", }); } ``` --- ## Route handler ```typescript app.post("/api/documents/pitch-deck", async (c) => { const data = PitchDeckSchema.parse(await c.req.json()); const url = await generatePitchDeck(c.env, data); return c.json({ url, expiresIn: 3600 }); }); ``` --- ## AI-assisted deck generation Use Workers AI to draft slide content from a brief, then pass to `generatePitchDeck`: ```typescript app.post("/api/documents/pitch-deck/ai-draft", async (c) => { const { brief, companyName } = await c.req.json(); const response = await c.env.AI.run("@cf/meta/llama-3.3-70b-instruct-fp8-fast", { messages: [ { role: "system", content: "You are a startup pitch deck writer. Output JSON matching the PitchDeckData schema.", }, { role: "user", content: `Draft a 7-slide pitch deck for: ${brief}. Company: ${companyName}. Output valid JSON only.`, }, ], response_format: { type: "json_object" }, }); const deckData = PitchDeckSchema.parse(JSON.parse((response as { response: string }).response)); const url = await generatePitchDeck(c.env, deckData); return c.json({ url, data: deckData }); }); ``` --- ## Worker compatibility notes - `pptxgenjs` v3.x: pure JS, no native deps, Worker-compatible - `pptx.write({ outputType: "arraybuffer" })` returns a Promise — await it - Add `compatibility_flags = ["nodejs_compat"]` to `wrangler.toml` - Slide image embeds: fetch image → convert to base64 → `slide.addImage({ data: base64string, ... })` - Maximum practical deck size in Workers: ~50 slides, ~20MB output — use Queues for larger -
SKILL.md 3 KB
--- name: "document-processing" description: "PDF/DOCX/XLSX/PPTX generation and parsing on Cloudflare Workers. Covers CF Browser Rendering → PDF, pdf-lib Worker-native generation, docx/exceljs output, pptxgenjs slides, and RAG-ready text extraction. Use cases: donor annual reports, SaaS invoices, tax receipts, financial reports, grant applications. Source: anthropics/skills pdf/docx/pptx/xlsx patterns." when_to_use: "Any request involving document output (invoice, report, receipt, export) or document input (parse a PDF, extract tables, RAG ingestion from uploaded docs)." effort: "high" model: "inherit" priority: 5 pack: "documents" stage: stable triggers: - "PDF" - "invoice" - "receipt" - "annual report" - "DOCX" - "XLSX" - "spreadsheet" - "PPTX" - "slide deck" - "parse document" - "extract text" - "RAG ingestion" paths: - "src/worker/**" - "apps/**" - "workers/**" --- # 18 — Document Processing Worker-native document I/O. All generation runs at the edge — no Lambda, no container, no third-party conversion SaaS. ## Sub-modules - `pdf-generation.md` — CF Browser Rendering → PDF + pdf-lib fallback - `pdf-parsing.md` — text + table extraction for RAG ingestion - `docx-xlsx.md` — DOCX (docx library) + XLSX (exceljs) generation in Workers - `pptx-generation.md` — PPTX via pptxgenjs in Workers ## Decision tree ``` Need document output? ├── PDF (invoice / receipt / report) │ ├── Complex layout (HTML → PDF) → CF Browser Rendering │ └── Programmatic (no layout) → pdf-lib in Worker ├── DOCX / XLSX (data export / mail merge) │ ├── DOCX → docx library (pure JS, Worker-compat) │ └── XLSX → exceljs (no canvas dep, Worker-compat) └── PPTX (slide deck / pitch deck) └── pptxgenjs (Worker-compat, no native deps) Need document input (RAG)? ├── PDF text → pdf-parse (pure JS) or Workers AI document extraction └── Tables → structured JSON → D1 or Vectorize ``` ## Cloudflare primitives used - `CF Browser Rendering` — puppeteer-compatible Workers binding for HTML → PDF - `R2` — store and serve generated documents - `D1` — job state + document metadata - `Workers AI` — optional OCR for scanned PDFs (Llama Vision) - `Queues` — async generation jobs (large reports) ## Use case map | Use case | Format | Method | |---|---|---| | SaaS invoice | PDF | pdf-lib → R2 | | Tax receipt (nonprofit) | PDF | CF Browser Rendering → R2 | | Donor annual report | PDF | CF Browser Rendering (full layout) | | Financial export | XLSX | exceljs → R2 | | Grant application | DOCX | docx → R2 | | Board slide deck | PPTX | pptxgenjs → R2 | | RAG: donor docs | Text | pdf-parse → Vectorize | ## Cross-links - `rules/cloudflare-lock-in-is-leverage.md` — CF Browser Rendering over puppeteer SaaS - `rules/feature-flags.md` — gate new doc types behind flag before GA - `13-observability-and-growth/` — track document generation events in PostHog - `08-deploy-and-runtime-verification/` — smoke-test R2 presigned URL after deploy
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.