office by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill office状态:生产就绪 最后更新:2026-01-12 依赖项:无(纯 JavaScript 库) 最新版本:docx@9.5.0, xlsx@0.18.5, pdf-lib@1.17.1, pptxgenjs@4.0.1
使用 TypeScript 以编程方式生成 Microsoft Office 文档和 PDF。所有库都是纯 JavaScript,无需任何原生依赖,支持所有运行时环境:
| 格式 | 库 | Workers | 浏览器 | Node.js |
|---|---|---|---|---|
| DOCX | docx | ✅ | ✅ | ✅ |
| XLSX | xlsx (SheetJS) | ✅ | ✅ | ✅ |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
pdf-lib | ✅ | ✅ | ✅ |
| PPTX | pptxgenjs | ⚠️* | ✅ | ✅ |
*Workers 中的 PPTX:适用于本地图像/数据。远程图像获取需要变通方案(使用 https 模块)。
# 安装全部四个(或根据需要选择)
npm install docx xlsx pdf-lib pptxgenjs
import { Document, Packer, Paragraph, TextRun } from 'docx';
import { writeFileSync } from 'fs';
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [
new TextRun({ text: 'Hello World', bold: true, size: 48 }),
],
}),
],
}],
});
// Node.js:保存到文件
const buffer = await Packer.toBuffer(doc);
writeFileSync('hello.docx', buffer);
// 浏览器/Workers:获取为 blob
const blob = await Packer.toBlob(doc);
import * as XLSX from 'xlsx';
// 创建包含数据的工作簿
const data = [
['Name', 'Amount', 'Date'],
['Invoice #1', 1500, '2026-01-12'],
['Invoice #2', 2300, '2026-01-13'],
];
const worksheet = XLSX.utils.aoa_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Invoices');
// Node.js:保存到文件
XLSX.writeFile(workbook, 'invoices.xlsx');
// 浏览器/Workers:获取为 buffer
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]); // Letter 尺寸
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
page.drawText('Hello World', {
x: 50,
y: 700,
size: 24,
font,
color: rgb(0, 0, 0),
});
// 获取为字节(适用于所有环境)
const pdfBytes = await pdfDoc.save();
// Node.js:保存到文件
writeFileSync('hello.pdf', pdfBytes);
import pptxgen from 'pptxgenjs';
const pptx = new pptxgen();
pptx.author = 'Your Name';
pptx.title = 'Sample Presentation';
// 添加幻灯片
const slide = pptx.addSlide();
slide.addText('Hello World', {
x: 1, y: 1, w: 8, h: 1.5,
fontSize: 36, bold: true, color: '363636',
});
// Node.js:保存到文件
await pptx.writeFile({ fileName: 'hello.pptx' });
// 浏览器:触发下载
await pptx.writeFile({ fileName: 'hello.pptx' });
docx 包使用构建器模式:
import { Document, Packer, Paragraph, TextRun, HeadingLevel } from 'docx';
const doc = new Document({
sections: [{
children: [
new Paragraph({
text: 'Monthly Report',
heading: HeadingLevel.HEADING_1,
}),
new Paragraph({
children: [
new TextRun('This is a '),
new TextRun({ text: 'bold', bold: true }),
new TextRun(' and '),
new TextRun({ text: 'italic', italics: true }),
new TextRun(' text example.'),
],
}),
],
}],
});
import { Document, Table, TableRow, TableCell, Paragraph, WidthType } from 'docx';
const table = new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [
new TableCell({ children: [new Paragraph('Header 1')] }),
new TableCell({ children: [new Paragraph('Header 2')] }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph('Data 1')] }),
new TableCell({ children: [new Paragraph('Data 2')] }),
],
}),
],
});
const doc = new Document({
sections: [{ children: [table] }],
});
import { Document, Paragraph, ImageRun } from 'docx';
import { readFileSync } from 'fs';
const imageBuffer = readFileSync('logo.png');
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [
new ImageRun({
data: imageBuffer,
transformation: { width: 200, height: 100 },
type: 'png',
}),
],
}),
],
}],
});
// Node.js - 保存到文件
import { writeFileSync } from 'fs';
const buffer = await Packer.toBuffer(doc);
writeFileSync('document.docx', buffer);
// 浏览器 - 下载
const blob = await Packer.toBlob(doc);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.docx';
a.click();
// Cloudflare Workers - 作为 Response 返回
const buffer = await Packer.toBuffer(doc);
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': 'attachment; filename="document.docx"',
},
});
SheetJS (xlsx) 使用实用函数:
import * as XLSX from 'xlsx';
const data = [
['Product', 'Price', 'Quantity', 'Total'],
['Widget A', 10, 5, { f: 'B2*C2' }], // 公式
['Widget B', 15, 3, { f: 'B3*C3' }],
['', '', 'Grand Total:', { f: 'SUM(D2:D3)' }],
];
const ws = XLSX.utils.aoa_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sales');
const invoices = [
{ id: 1, customer: 'Acme Corp', amount: 1500, date: '2026-01-12' },
{ id: 2, customer: 'Globex', amount: 2300, date: '2026-01-13' },
];
const ws = XLSX.utils.json_to_sheet(invoices);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Invoices');
// 设置列宽(以字符为单位)
ws['!cols'] = [
{ wch: 10 }, // 列 A
{ wch: 20 }, // 列 B
{ wch: 15 }, // 列 C
];
const wb = XLSX.utils.book_new();
const summaryData = [['Total Sales', 3800]];
const detailData = [['Item', 'Amount'], ['Item 1', 1500], ['Item 2', 2300]];
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(summaryData), 'Summary');
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(detailData), 'Details');
// Node.js - 保存到文件
XLSX.writeFile(workbook, 'report.xlsx');
// 浏览器/Workers - 获取 buffer
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
// Cloudflare Workers - 作为 Response 返回
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': 'attachment; filename="report.xlsx"',
},
});
pdf-lib 使用基于页面的方法:
// 常见尺寸(以点为单位)[宽度, 高度]
const LETTER = [612, 792]; // 8.5" x 11"
const A4 = [595.28, 841.89]; // 210mm x 297mm
const LEGAL = [612, 1008]; // 8.5" x 14"
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]);
// 嵌入标准字体
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// 绘制文本(y=0 是页面底部)
page.drawText('Invoice #12345', {
x: 50,
y: 750, // 靠近页面顶部
size: 24,
font: helveticaBold,
color: rgb(0, 0, 0),
});
page.drawText('Thank you for your business!', {
x: 50,
y: 50, // 靠近页面底部
size: 12,
font: helvetica,
color: rgb(0.5, 0.5, 0.5),
});
// 矩形
page.drawRectangle({
x: 50,
y: 600,
width: 200,
height: 100,
borderColor: rgb(0, 0, 0),
borderWidth: 1,
color: rgb(0.95, 0.95, 0.95), // 填充颜色
});
// 线条
page.drawLine({
start: { x: 50, y: 500 },
end: { x: 550, y: 500 },
thickness: 1,
color: rgb(0, 0, 0),
});
// 从 URL/fetch 获取
const imageBytes = await fetch('https://example.com/logo.png').then(r => r.arrayBuffer());
const image = await pdfDoc.embedPng(imageBytes);
// 或使用 embedJpg 处理 JPEG
// const image = await pdfDoc.embedJpg(imageBytes);
page.drawImage(image, {
x: 50,
y: 700,
width: 100,
height: 50,
});
import { PDFDocument } from 'pdf-lib';
// 加载带有表单的现有 PDF
const existingPdfBytes = await fetch('form.pdf').then(r => r.arrayBuffer());
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// 获取表单和字段
const form = pdfDoc.getForm();
const nameField = form.getTextField('customer_name');
const dateField = form.getTextField('date');
// 填写字段
nameField.setText('John Doe');
dateField.setText('2026-01-12');
// 展平表单(使字段不可编辑)
form.flatten();
const pdfBytes = await pdfDoc.save();
import { PDFDocument } from 'pdf-lib';
const pdf1Bytes = await fetch('doc1.pdf').then(r => r.arrayBuffer());
const pdf2Bytes = await fetch('doc2.pdf').then(r => r.arrayBuffer());
const mergedPdf = await PDFDocument.create();
const pdf1 = await PDFDocument.load(pdf1Bytes);
const pdf2 = await PDFDocument.load(pdf2Bytes);
// 从两个文档复制所有页面
const pages1 = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
const pages2 = await mergedPdf.copyPages(pdf2, pdf2.getPageIndices());
pages1.forEach(page => mergedPdf.addPage(page));
pages2.forEach(page => mergedPdf.addPage(page));
const mergedBytes = await mergedPdf.save();
pptxgenjs 使用基于幻灯片的方法:
import pptxgen from 'pptxgenjs';
const pptx = new pptxgen();
pptx.author = 'Author Name';
pptx.title = 'Presentation Title';
pptx.subject = 'Subject';
pptx.company = 'Company';
// 标题幻灯片
const titleSlide = pptx.addSlide();
titleSlide.addText('Quarterly Report', {
x: 0.5, y: 2, w: 9, h: 1,
fontSize: 44, bold: true, color: '0066CC', align: 'center',
});
titleSlide.addText('Q1 2026', {
x: 0.5, y: 3.5, w: 9, h: 0.5,
fontSize: 24, color: '666666', align: 'center',
});
const contentSlide = pptx.addSlide();
// 标题
contentSlide.addText('Key Highlights', {
x: 0.5, y: 0.5, w: 9, h: 0.8,
fontSize: 32, bold: true, color: '333333',
});
// 项目符号
contentSlide.addText([
{ text: 'Revenue up 25% YoY', options: { bullet: true, fontSize: 20 } },
{ text: 'Customer base grew to 10,000', options: { bullet: true, fontSize: 20 } },
{ text: 'New product launch successful', options: { bullet: true, fontSize: 20 } },
{ text: 'Expanded to 5 new markets', options: { bullet: true, fontSize: 20 } },
], { x: 0.5, y: 1.5, w: 8, h: 4, valign: 'top' });
const tableSlide = pptx.addSlide();
tableSlide.addText('Sales Summary', {
x: 0.5, y: 0.5, w: 9, h: 0.8,
fontSize: 28, bold: true,
});
const tableData = [
[{ text: 'Region', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } },
{ text: 'Q1', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } },
{ text: 'Q2', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } }],
['North America', '$2.5M', '$2.8M'],
['Europe', '$1.8M', '$2.1M'],
['Asia Pacific', '$1.2M', '$1.5M'],
];
tableSlide.addTable(tableData, {
x: 0.5, y: 1.5, w: 9,
border: { pt: 1, color: 'CCCCCC' },
fontFace: 'Arial',
fontSize: 14,
align: 'center',
valign: 'middle',
});
const chartSlide = pptx.addSlide();
chartSlide.addText('Revenue Trend', {
x: 0.5, y: 0.5, w: 9, h: 0.6,
fontSize: 28, bold: true,
});
chartSlide.addChart(pptx.ChartType.line, [
{ name: 'Revenue', labels: ['Jan', 'Feb', 'Mar', 'Apr'], values: [100, 120, 150, 180] },
{ name: 'Expenses', labels: ['Jan', 'Feb', 'Mar', 'Apr'], values: [80, 85, 90, 95] },
], {
x: 0.5, y: 1.2, w: 9, h: 4,
showLegend: true,
legendPos: 'b',
showTitle: false,
});
import { readFileSync } from 'fs';
const imageSlide = pptx.addSlide();
// 从文件(Node.js)
imageSlide.addImage({
path: 'logo.png',
x: 0.5, y: 0.5, w: 2, h: 1,
});
// 从 base64
const imageBase64 = readFileSync('chart.png').toString('base64');
imageSlide.addImage({
data: `image/png;base64,${imageBase64}`,
x: 0.5, y: 2, w: 4, h: 3,
});
// 从 URL(仅限 Node.js - 使用 https 模块)
imageSlide.addImage({
path: 'https://example.com/image.png',
x: 5, y: 2, w: 4, h: 3,
});
const shapeSlide = pptx.addSlide();
// 矩形
shapeSlide.addShape(pptx.ShapeType.rect, {
x: 0.5, y: 0.5, w: 3, h: 2,
fill: { color: '0066CC' },
line: { color: '004499', pt: 2 },
});
// 圆形/椭圆形
shapeSlide.addShape(pptx.ShapeType.ellipse, {
x: 4, y: 0.5, w: 2, h: 2,
fill: { color: '00AA00' },
});
// 箭头
shapeSlide.addShape(pptx.ShapeType.rightArrow, {
x: 1, y: 3, w: 3, h: 1,
fill: { color: 'FF6600' },
});
// 定义母版幻灯片
pptx.defineSlideMaster({
title: 'COMPANY_MASTER',
background: { color: 'FFFFFF' },
objects: [
{ text: { text: 'Company Name', options: { x: 0.5, y: 0.2, w: 4, h: 0.3, fontSize: 10, color: '999999' } } },
{ line: { x: 0.5, y: 0.6, w: 9, h: 0, line: { color: '0066CC', pt: 2 } } },
],
});
// 使用母版
const slide = pptx.addSlide({ masterName: 'COMPANY_MASTER' });
// Node.js - 保存到文件
await pptx.writeFile({ fileName: 'presentation.pptx' });
// 浏览器 - 触发下载
await pptx.writeFile({ fileName: 'presentation.pptx' });
// 获取为 base64(用于电子邮件、API 等)
const base64 = await pptx.write({ outputType: 'base64' });
// 获取为 Blob(浏览器)
const blob = await pptx.write({ outputType: 'blob' });
// 获取为 ArrayBuffer
const arrayBuffer = await pptx.write({ outputType: 'arraybuffer' });
// Cloudflare Workers - 作为 Response 返回
const arrayBuffer = await pptx.write({ outputType: 'arraybuffer' });
return new Response(arrayBuffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'Content-Disposition': 'attachment; filename="presentation.pptx"',
},
});
所有四个库都可在 Cloudflare Workers 中运行(PPTX 对于远程图像有注意事项)。
import { Hono } from 'hono';
import { Document, Packer, Paragraph, TextRun } from 'docx';
const app = new Hono();
app.get('/api/invoice/:id', async (c) => {
const invoiceId = c.req.param('id');
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [new TextRun({ text: `Invoice #${invoiceId}`, bold: true, size: 48 })],
}),
],
}],
});
const buffer = await Packer.toBuffer(doc);
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': `attachment; filename="invoice-${invoiceId}.docx"`,
},
});
});
export default app;
对于包含 CSS 的复杂布局,使用 Cloudflare 浏览器渲染(付费功能):
import puppeteer from '@cloudflare/puppeteer';
export default {
async fetch(request, env) {
const browser = await puppeteer.launch(env.BROWSER);
const page = await browser.newPage();
// 设置 HTML 内容
await page.setContent(`
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 40px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; }
td, th { border: 1px solid #ddd; padding: 8px; }
</style>
</head>
<body>
<h1>Invoice #12345</h1>
<table>
<tr><th>Item</th><th>Amount</th></tr>
<tr><td>Service</td><td>$500</td></tr>
</table>
</body>
</html>
`);
const pdf = await page.pdf({ format: 'A4' });
await browser.close();
return new Response(pdf, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="invoice.pdf"',
},
});
},
};
wrangler.jsonc 绑定:
{
"browser": {
"binding": "BROWSER"
}
}
✅ 对 DOCX 使用 await Packer.toBuffer()(它是异步的) ✅ 记住 PDF 坐标从 BOTTOM-LEFT 开始 ✅ 在 Workers/浏览器中对 XLSX 使用 type: 'buffer' ✅ 在 PDF 中使用字体前先嵌入它们 ✅ 为下载设置正确的 Content-Type 头部 ✅ 对 PPTX 使用 await pptx.writeFile() 或 await pptx.write() ✅ 在 Workers 中对 PPTX 使用 base64 图像(避免远程 URL)
❌ 不使用 await 调用 Packer.toBuffer()(返回 Promise) ❌ 假设 PDF y=0 在顶部(它在底部) ❌ 在 Workers 中使用 writeFile(改用 Response) ❌ 忘记为下载设置 Content-Disposition ❌ 在浏览器/Workers 中使用 Node.js fs 模块 ❌ 在 Workers 中使用 PPTX 路径 URL(https 模块不可用)
错误:文件中的 [object Promise] 或空文档 原因:Packer.toBuffer() 是异步的,但调用时没有 await 预防:始终使用 await Packer.toBuffer(doc)
错误:文本出现在页面底部或页面外 原因:PDF 坐标原点在左下角,而不是左上角 预防:对于靠近顶部的文本,使用较高的 y 值(例如,对于 letter 尺寸使用 y=750)
错误:单元格显示公式文本而不是结果 原因:SheetJS 不执行公式,Excel 在打开时执行 预防:这是预期行为 - 公式在 Excel 中打开时计算
错误:下载的文件为空或损坏 原因:返回了错误的类型或缺少 Content-Type 头部 预防:直接返回 buffer 并设置正确的头部
错误:https is not defined 或图像未显示 原因:pptxgenjs 使用 Node.js https 模块获取远程图像 预防:在 Workers 环境中使用 base64 数据 URI 或本地图像
docx-basic.ts - 包含标题、表格、图像的完整 Word 文档xlsx-basic.ts - 包含公式和格式的 Excel 工作簿pdf-basic.ts - 包含文本、图像、形状的 PDFpptx-basic.ts - 包含幻灯片、图表、表格的 PowerPointworkers-pdf.ts - Cloudflare Workers PDF 生成示例docx-api.md - docx npm 包的快速参考xlsx-api.md - SheetJS 函数的快速参考pdf-lib-api.md - pdf-lib 方法的快速参考pptxgenjs-api.md - pptxgenjs 的快速参考verify-deps.sh - 检查库版本是否为最新此技能专注于使用可移植的 TypeScript 库进行文档创建:
| 功能 | 此技能 | Anthropic 的技能 |
|---|---|---|
| 创建 DOCX/XLSX/PDF/PPTX | ✅ | ✅ |
| 在 Cloudflare Workers 中运行 | ✅ | ❌ |
| 可安装插件 | ✅ | ❌ |
| TypeScript 优先 | ✅ | ❌ (Python) |
| 编辑带有修订跟踪的现有 DOCX | ❌ | ✅ |
| Excel 公式验证 | ❌ | ✅ |
| OCR 扫描的 PDF | ❌ | ✅ |
对于高级编辑场景(修订跟踪、公式验证),请考虑使用带有 Python 工具的 Anthropic 官方技能。
{
"dependencies": {
"docx": "^9.5.0",
"xlsx": "^0.18.5",
"pdf-lib": "^1.17.1",
"pptxgenjs": "^4.0.1"
}
}
npm install docx xlsx pdf-lib)每周安装数
421
仓库
GitHub 星标数
650
首次出现
Jan 20, 2026
安全审计
安装于
claude-code326
opencode279
gemini-cli267
cursor243
antigravity232
codex225
Status : Production Ready Last Updated : 2026-01-12 Dependencies : None (pure JavaScript libraries) Latest Versions : docx@9.5.0, xlsx@0.18.5, pdf-lib@1.17.1, pptxgenjs@4.0.1
Generate Microsoft Office documents and PDFs programmatically with TypeScript. All libraries are pure JavaScript with zero native dependencies, enabling universal runtime support:
| Format | Library | Workers | Browser | Node.js |
|---|---|---|---|---|
| DOCX | docx | ✅ | ✅ | ✅ |
| XLSX | xlsx (SheetJS) | ✅ | ✅ | ✅ |
pdf-lib | ✅ | ✅ | ✅ | |
| PPTX | pptxgenjs | ⚠️* | ✅ | ✅ |
*PPTX in Workers: Works for local images/data. Remote image fetching needs workaround (uses https module).
# Install all four (or pick what you need)
npm install docx xlsx pdf-lib pptxgenjs
import { Document, Packer, Paragraph, TextRun } from 'docx';
import { writeFileSync } from 'fs';
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [
new TextRun({ text: 'Hello World', bold: true, size: 48 }),
],
}),
],
}],
});
// Node.js: Save to file
const buffer = await Packer.toBuffer(doc);
writeFileSync('hello.docx', buffer);
// Browser/Workers: Get as blob
const blob = await Packer.toBlob(doc);
import * as XLSX from 'xlsx';
// Create workbook with data
const data = [
['Name', 'Amount', 'Date'],
['Invoice #1', 1500, '2026-01-12'],
['Invoice #2', 2300, '2026-01-13'],
];
const worksheet = XLSX.utils.aoa_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Invoices');
// Node.js: Save to file
XLSX.writeFile(workbook, 'invoices.xlsx');
// Browser/Workers: Get as buffer
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]); // Letter size
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
page.drawText('Hello World', {
x: 50,
y: 700,
size: 24,
font,
color: rgb(0, 0, 0),
});
// Get as bytes (works everywhere)
const pdfBytes = await pdfDoc.save();
// Node.js: Save to file
writeFileSync('hello.pdf', pdfBytes);
import pptxgen from 'pptxgenjs';
const pptx = new pptxgen();
pptx.author = 'Your Name';
pptx.title = 'Sample Presentation';
// Add a slide
const slide = pptx.addSlide();
slide.addText('Hello World', {
x: 1, y: 1, w: 8, h: 1.5,
fontSize: 36, bold: true, color: '363636',
});
// Node.js: Save to file
await pptx.writeFile({ fileName: 'hello.pptx' });
// Browser: Trigger download
await pptx.writeFile({ fileName: 'hello.pptx' });
The docx package uses a builder pattern:
import { Document, Packer, Paragraph, TextRun, HeadingLevel } from 'docx';
const doc = new Document({
sections: [{
children: [
new Paragraph({
text: 'Monthly Report',
heading: HeadingLevel.HEADING_1,
}),
new Paragraph({
children: [
new TextRun('This is a '),
new TextRun({ text: 'bold', bold: true }),
new TextRun(' and '),
new TextRun({ text: 'italic', italics: true }),
new TextRun(' text example.'),
],
}),
],
}],
});
import { Document, Table, TableRow, TableCell, Paragraph, WidthType } from 'docx';
const table = new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [
new TableCell({ children: [new Paragraph('Header 1')] }),
new TableCell({ children: [new Paragraph('Header 2')] }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph('Data 1')] }),
new TableCell({ children: [new Paragraph('Data 2')] }),
],
}),
],
});
const doc = new Document({
sections: [{ children: [table] }],
});
import { Document, Paragraph, ImageRun } from 'docx';
import { readFileSync } from 'fs';
const imageBuffer = readFileSync('logo.png');
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [
new ImageRun({
data: imageBuffer,
transformation: { width: 200, height: 100 },
type: 'png',
}),
],
}),
],
}],
});
// Node.js - Save to file
import { writeFileSync } from 'fs';
const buffer = await Packer.toBuffer(doc);
writeFileSync('document.docx', buffer);
// Browser - Download
const blob = await Packer.toBlob(doc);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.docx';
a.click();
// Cloudflare Workers - Return as Response
const buffer = await Packer.toBuffer(doc);
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': 'attachment; filename="document.docx"',
},
});
SheetJS (xlsx) uses utility functions:
import * as XLSX from 'xlsx';
const data = [
['Product', 'Price', 'Quantity', 'Total'],
['Widget A', 10, 5, { f: 'B2*C2' }], // Formula
['Widget B', 15, 3, { f: 'B3*C3' }],
['', '', 'Grand Total:', { f: 'SUM(D2:D3)' }],
];
const ws = XLSX.utils.aoa_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sales');
const invoices = [
{ id: 1, customer: 'Acme Corp', amount: 1500, date: '2026-01-12' },
{ id: 2, customer: 'Globex', amount: 2300, date: '2026-01-13' },
];
const ws = XLSX.utils.json_to_sheet(invoices);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Invoices');
// Set column widths (in characters)
ws['!cols'] = [
{ wch: 10 }, // Column A
{ wch: 20 }, // Column B
{ wch: 15 }, // Column C
];
const wb = XLSX.utils.book_new();
const summaryData = [['Total Sales', 3800]];
const detailData = [['Item', 'Amount'], ['Item 1', 1500], ['Item 2', 2300]];
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(summaryData), 'Summary');
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(detailData), 'Details');
// Node.js - Save to file
XLSX.writeFile(workbook, 'report.xlsx');
// Browser/Workers - Get buffer
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
// Cloudflare Workers - Return as Response
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': 'attachment; filename="report.xlsx"',
},
});
pdf-lib uses a page-based approach:
// Common sizes in points [width, height]
const LETTER = [612, 792]; // 8.5" x 11"
const A4 = [595.28, 841.89]; // 210mm x 297mm
const LEGAL = [612, 1008]; // 8.5" x 14"
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]);
// Embed standard fonts
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// Draw text (y=0 is BOTTOM of page)
page.drawText('Invoice #12345', {
x: 50,
y: 750, // Near top of page
size: 24,
font: helveticaBold,
color: rgb(0, 0, 0),
});
page.drawText('Thank you for your business!', {
x: 50,
y: 50, // Near bottom of page
size: 12,
font: helvetica,
color: rgb(0.5, 0.5, 0.5),
});
// Rectangle
page.drawRectangle({
x: 50,
y: 600,
width: 200,
height: 100,
borderColor: rgb(0, 0, 0),
borderWidth: 1,
color: rgb(0.95, 0.95, 0.95), // Fill color
});
// Line
page.drawLine({
start: { x: 50, y: 500 },
end: { x: 550, y: 500 },
thickness: 1,
color: rgb(0, 0, 0),
});
// From URL/fetch
const imageBytes = await fetch('https://example.com/logo.png').then(r => r.arrayBuffer());
const image = await pdfDoc.embedPng(imageBytes);
// Or embedJpg for JPEG
// const image = await pdfDoc.embedJpg(imageBytes);
page.drawImage(image, {
x: 50,
y: 700,
width: 100,
height: 50,
});
import { PDFDocument } from 'pdf-lib';
// Load existing PDF with form
const existingPdfBytes = await fetch('form.pdf').then(r => r.arrayBuffer());
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Get form and fields
const form = pdfDoc.getForm();
const nameField = form.getTextField('customer_name');
const dateField = form.getTextField('date');
// Fill fields
nameField.setText('John Doe');
dateField.setText('2026-01-12');
// Flatten form (make fields non-editable)
form.flatten();
const pdfBytes = await pdfDoc.save();
import { PDFDocument } from 'pdf-lib';
const pdf1Bytes = await fetch('doc1.pdf').then(r => r.arrayBuffer());
const pdf2Bytes = await fetch('doc2.pdf').then(r => r.arrayBuffer());
const mergedPdf = await PDFDocument.create();
const pdf1 = await PDFDocument.load(pdf1Bytes);
const pdf2 = await PDFDocument.load(pdf2Bytes);
// Copy all pages from both documents
const pages1 = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
const pages2 = await mergedPdf.copyPages(pdf2, pdf2.getPageIndices());
pages1.forEach(page => mergedPdf.addPage(page));
pages2.forEach(page => mergedPdf.addPage(page));
const mergedBytes = await mergedPdf.save();
pptxgenjs uses a slide-based approach:
import pptxgen from 'pptxgenjs';
const pptx = new pptxgen();
pptx.author = 'Author Name';
pptx.title = 'Presentation Title';
pptx.subject = 'Subject';
pptx.company = 'Company';
// Title slide
const titleSlide = pptx.addSlide();
titleSlide.addText('Quarterly Report', {
x: 0.5, y: 2, w: 9, h: 1,
fontSize: 44, bold: true, color: '0066CC', align: 'center',
});
titleSlide.addText('Q1 2026', {
x: 0.5, y: 3.5, w: 9, h: 0.5,
fontSize: 24, color: '666666', align: 'center',
});
const contentSlide = pptx.addSlide();
// Title
contentSlide.addText('Key Highlights', {
x: 0.5, y: 0.5, w: 9, h: 0.8,
fontSize: 32, bold: true, color: '333333',
});
// Bullet points
contentSlide.addText([
{ text: 'Revenue up 25% YoY', options: { bullet: true, fontSize: 20 } },
{ text: 'Customer base grew to 10,000', options: { bullet: true, fontSize: 20 } },
{ text: 'New product launch successful', options: { bullet: true, fontSize: 20 } },
{ text: 'Expanded to 5 new markets', options: { bullet: true, fontSize: 20 } },
], { x: 0.5, y: 1.5, w: 8, h: 4, valign: 'top' });
const tableSlide = pptx.addSlide();
tableSlide.addText('Sales Summary', {
x: 0.5, y: 0.5, w: 9, h: 0.8,
fontSize: 28, bold: true,
});
const tableData = [
[{ text: 'Region', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } },
{ text: 'Q1', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } },
{ text: 'Q2', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } }],
['North America', '$2.5M', '$2.8M'],
['Europe', '$1.8M', '$2.1M'],
['Asia Pacific', '$1.2M', '$1.5M'],
];
tableSlide.addTable(tableData, {
x: 0.5, y: 1.5, w: 9,
border: { pt: 1, color: 'CCCCCC' },
fontFace: 'Arial',
fontSize: 14,
align: 'center',
valign: 'middle',
});
const chartSlide = pptx.addSlide();
chartSlide.addText('Revenue Trend', {
x: 0.5, y: 0.5, w: 9, h: 0.6,
fontSize: 28, bold: true,
});
chartSlide.addChart(pptx.ChartType.line, [
{ name: 'Revenue', labels: ['Jan', 'Feb', 'Mar', 'Apr'], values: [100, 120, 150, 180] },
{ name: 'Expenses', labels: ['Jan', 'Feb', 'Mar', 'Apr'], values: [80, 85, 90, 95] },
], {
x: 0.5, y: 1.2, w: 9, h: 4,
showLegend: true,
legendPos: 'b',
showTitle: false,
});
import { readFileSync } from 'fs';
const imageSlide = pptx.addSlide();
// From file (Node.js)
imageSlide.addImage({
path: 'logo.png',
x: 0.5, y: 0.5, w: 2, h: 1,
});
// From base64
const imageBase64 = readFileSync('chart.png').toString('base64');
imageSlide.addImage({
data: `image/png;base64,${imageBase64}`,
x: 0.5, y: 2, w: 4, h: 3,
});
// From URL (Node.js only - uses https module)
imageSlide.addImage({
path: 'https://example.com/image.png',
x: 5, y: 2, w: 4, h: 3,
});
const shapeSlide = pptx.addSlide();
// Rectangle
shapeSlide.addShape(pptx.ShapeType.rect, {
x: 0.5, y: 0.5, w: 3, h: 2,
fill: { color: '0066CC' },
line: { color: '004499', pt: 2 },
});
// Circle/Oval
shapeSlide.addShape(pptx.ShapeType.ellipse, {
x: 4, y: 0.5, w: 2, h: 2,
fill: { color: '00AA00' },
});
// Arrow
shapeSlide.addShape(pptx.ShapeType.rightArrow, {
x: 1, y: 3, w: 3, h: 1,
fill: { color: 'FF6600' },
});
// Define a master slide
pptx.defineSlideMaster({
title: 'COMPANY_MASTER',
background: { color: 'FFFFFF' },
objects: [
{ text: { text: 'Company Name', options: { x: 0.5, y: 0.2, w: 4, h: 0.3, fontSize: 10, color: '999999' } } },
{ line: { x: 0.5, y: 0.6, w: 9, h: 0, line: { color: '0066CC', pt: 2 } } },
],
});
// Use the master
const slide = pptx.addSlide({ masterName: 'COMPANY_MASTER' });
// Node.js - Save to file
await pptx.writeFile({ fileName: 'presentation.pptx' });
// Browser - Trigger download
await pptx.writeFile({ fileName: 'presentation.pptx' });
// Get as base64 (for email, API, etc.)
const base64 = await pptx.write({ outputType: 'base64' });
// Get as Blob (browser)
const blob = await pptx.write({ outputType: 'blob' });
// Get as ArrayBuffer
const arrayBuffer = await pptx.write({ outputType: 'arraybuffer' });
// Cloudflare Workers - Return as Response
const arrayBuffer = await pptx.write({ outputType: 'arraybuffer' });
return new Response(arrayBuffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'Content-Disposition': 'attachment; filename="presentation.pptx"',
},
});
All four libraries work in Cloudflare Workers (PPTX with caveats for remote images).
import { Hono } from 'hono';
import { Document, Packer, Paragraph, TextRun } from 'docx';
const app = new Hono();
app.get('/api/invoice/:id', async (c) => {
const invoiceId = c.req.param('id');
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [new TextRun({ text: `Invoice #${invoiceId}`, bold: true, size: 48 })],
}),
],
}],
});
const buffer = await Packer.toBuffer(doc);
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': `attachment; filename="invoice-${invoiceId}.docx"`,
},
});
});
export default app;
For complex layouts with CSS, use Cloudflare Browser Rendering (paid feature):
import puppeteer from '@cloudflare/puppeteer';
export default {
async fetch(request, env) {
const browser = await puppeteer.launch(env.BROWSER);
const page = await browser.newPage();
// Set HTML content
await page.setContent(`
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 40px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; }
td, th { border: 1px solid #ddd; padding: 8px; }
</style>
</head>
<body>
<h1>Invoice #12345</h1>
<table>
<tr><th>Item</th><th>Amount</th></tr>
<tr><td>Service</td><td>$500</td></tr>
</table>
</body>
</html>
`);
const pdf = await page.pdf({ format: 'A4' });
await browser.close();
return new Response(pdf, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="invoice.pdf"',
},
});
},
};
wrangler.jsonc binding:
{
"browser": {
"binding": "BROWSER"
}
}
✅ Use await Packer.toBuffer() for DOCX (it's async) ✅ Remember PDF coordinates start at BOTTOM-LEFT ✅ Use type: 'buffer' for XLSX in Workers/browser ✅ Embed fonts in PDF before using them ✅ Set proper Content-Type headers for downloads ✅ Use await pptx.writeFile() or await pptx.write() for PPTX ✅ Use base64 images in PPTX for Workers (avoid remote URLs)
❌ Use Packer.toBuffer() without await (returns Promise) ❌ Assume PDF y=0 is at top (it's at bottom) ❌ Use writeFile in Workers (use Response instead) ❌ Forget to set Content-Disposition for downloads ❌ Use Node.js fs module in browser/Workers ❌ Use PPTX path URLs in Workers (https module not available)
Error : [object Promise] in file or empty document Why : Packer.toBuffer() is async but called without await Prevention : Always await Packer.toBuffer(doc)
Error : Text appears at bottom of page or off-page Why : PDF coordinates have origin at bottom-left, not top-left Prevention : For text near top, use high y values (e.g., y=750 for letter size)
Error : Cell shows formula text instead of result Why : SheetJS doesn't execute formulas, Excel does on open Prevention : This is expected - formulas calculate when opened in Excel
Error : Downloaded file is empty or corrupted Why : Returning wrong type or missing Content-Type header Prevention : Return buffer directly with proper headers
Error : https is not defined or image not appearing Why : pptxgenjs uses Node.js https module for remote images Prevention : Use base64 data URIs or local images in Workers environment
docx-basic.ts - Complete Word document with headings, tables, imagesxlsx-basic.ts - Excel workbook with formulas and formattingpdf-basic.ts - PDF with text, images, shapespptx-basic.ts - PowerPoint with slides, charts, tablesworkers-pdf.ts - Cloudflare Workers PDF generation exampledocx-api.md - Quick reference for docx npm packagexlsx-api.md - Quick reference for SheetJS functionspdf-lib-api.md - Quick reference for pdf-lib methodspptxgenjs-api.md - Quick reference for pptxgenjsverify-deps.sh - Check library versions are currentThis skill focuses on document creation with portable TypeScript libraries:
| Feature | This Skill | Anthropic's Skills |
|---|---|---|
| Create DOCX/XLSX/PDF/PPTX | ✅ | ✅ |
| Works in Cloudflare Workers | ✅ | ❌ |
| Plugin installable | ✅ | ❌ |
| TypeScript-first | ✅ | ❌ (Python) |
| Edit existing DOCX with tracked changes | ❌ | ✅ |
| Excel formula validation | ❌ | ✅ |
| OCR scanned PDFs | ❌ | ✅ |
For advanced editing scenarios (tracked changes, formula validation), consider Anthropic's official skills with Python tooling.
{
"dependencies": {
"docx": "^9.5.0",
"xlsx": "^0.18.5",
"pdf-lib": "^1.17.1",
"pptxgenjs": "^4.0.1"
}
}
npm install docx xlsx pdf-lib)Weekly Installs
421
Repository
GitHub Stars
650
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubWarnSocketPassSnykWarn
Installed on
claude-code326
opencode279
gemini-cli267
cursor243
antigravity232
codex225