Claude Cursor GitHub Copilot opencode Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download heymegabyte-claude-skills-18-document-processing-e7acb91.zip · 10 KB
Part of heymegabyte/claude-skills — 18 skills

Install

skills CLI npx skills add https://github.com/heymegabyte/claude-skills/tree/master/18-document-processing
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install heymegabyte-claude-skills@llmmart
Git 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 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
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.

No comments yet.

Reviews (0)

No reviews yet.

Related