
Document Skills
- 3.1k installs
- 86 repo stars
- Updated July 17, 2026
- travisjneuman/.claude
document-skills is an agent skill suite that routes document work to docx, pdf, pptx, and xlsx sub-skills for Office-format creation and analysis.
About
document-skills is a travisjneuman/.claude suite for professional document creation, editing, and analysis across Office formats and PDFs. It routes work to four sub-skills: docx for Word creation and tracked changes, pdf for text extraction and merge or split, pptx for presentation layouts and charts, and xlsx for spreadsheet formulas and analysis. The parent skill instructs agents to identify the needed format, load the matching sub-skill such as Skill(document-skills/docx), and follow that sub-skill workflow. docx creation uses docx-js in JavaScript or TypeScript with Document, Packer, Paragraph, and TextRun APIs, stressing separate Paragraph elements instead of newline characters to avoid corrupt Word output. pdf covers extraction, tables, metadata, and merge or split operations. pptx handles layouts and charts while xlsx supports formulas and data analysis. Use document-skills when agent workflows must emit client-ready Word reports, proposals, manuals, decks, or spreadsheets rather than Markdown-only exports. Each sub-skill maintains its own SKILL.md with full implementation detail.
- Parent router for docx, pdf, pptx, and xlsx sub-skills with dedicated workflows.
- docx sub-skill uses docx-js with Document, Packer, Paragraph, and TextRun APIs.
- pdf sub-skill supports text extraction, tables, metadata, merge, and split.
- pptx sub-skill covers presentation creation, layouts, and charts.
- xlsx sub-skill handles spreadsheet manipulation, formulas, and analysis.
Document Skills by the numbers
- 3,113 all-time installs (skills.sh)
- +28 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #130 of 1,879 Documentation skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
document-skills capabilities & compatibility
- Capabilities
- format routing to docx, pdf, pptx, and xlsx sub · word document creation with docx js apis · pdf extraction, merge, and split · presentation and spreadsheet manipulation
- Use cases
- presentations
What document-skills says it does
Professional document creation, editing, and analysis for Office formats
npx skills add https://github.com/travisjneuman/.claude --skill document-skillsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.1k |
|---|---|
| repo stars | ★ 86 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 17, 2026 |
| Repository | travisjneuman/.claude ↗ |
How do I generate or edit Word, PDF, PowerPoint, and Excel files correctly from an agent workflow?
Generate properly formatted.docx reports, proposals, and user manuals directly from agent workflows.
Who is it for?
Developers automating client-ready Office documents across Word, PDF, slides, and spreadsheets in agent sessions.
Skip if: Skip when you only need plain Markdown exports without Office binary formats.
When should I use this skill?
User asks to create docx reports, extract PDF tables, build pptx decks, or manipulate xlsx spreadsheets in agent workflows.
What you get
Correctly formatted office documents created or analyzed via the appropriate docx, pdf, pptx, or xlsx sub-skill workflow.
- docx, pdf, pptx, or xlsx artifacts
Files
DOCX creation, editing, and analysis
Overview
A user may ask you to create, edit, or analyze the contents of a .docx file. A .docx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks.
Workflow Decision Tree
Reading/Analyzing Content
Use "Text extraction" or "Raw XML access" sections below
Creating New Document
Use "Creating a new Word document" workflow
Editing Existing Document
- Your own document + simple changes
Use "Basic OOXML editing" workflow
- Someone else's document
Use "Redlining workflow" (recommended default)
- Legal, academic, business, or government docs
Use "Redlining workflow" (required)
Reading and analyzing content
Text extraction
If you just need to read the text contents of a document, you should convert the document to markdown using pandoc. Pandoc provides excellent support for preserving document structure and can show tracked changes:
# Convert document to markdown with tracked changes
pandoc --track-changes=all path-to-file.docx -o output.md
# Options: --track-changes=accept/reject/allRaw XML access
You need raw XML access for: comments, complex formatting, document structure, embedded media, and metadata. For any of these features, you'll need to unpack a document and read its raw XML contents.
Unpacking a file
python ooxml/scripts/unpack.py <office_file> <output_directory>
Key file structures
word/document.xml- Main document contentsword/comments.xml- Comments referenced in document.xmlword/media/- Embedded images and media files- Tracked changes use
<w:ins>(insertions) and<w:del>(deletions) tags
Creating a new Word document
When creating a new Word document from scratch, use docx-js, which allows you to create Word documents using JavaScript/TypeScript.
Workflow
1. MANDATORY - READ ENTIRE FILE: Read `docx-js.md` (~500 lines) completely from start to finish. NEVER set any range limits when reading this file. Read the full file content for detailed syntax, critical formatting rules, and best practices before proceeding with document creation. 2. Create a JavaScript/TypeScript file using Document, Paragraph, TextRun components (You can assume all dependencies are installed, but if not, refer to the dependencies section below) 3. Export as .docx using Packer.toBuffer()
Editing an existing Word document
When editing an existing Word document, use the Document library (a Python library for OOXML manipulation). The library automatically handles infrastructure setup and provides methods for document manipulation. For complex scenarios, you can access the underlying DOM directly through the library.
Workflow
1. MANDATORY - READ ENTIRE FILE: Read `ooxml.md` (~600 lines) completely from start to finish. NEVER set any range limits when reading this file. Read the full file content for the Document library API and XML patterns for directly editing document files. 2. Unpack the document: python ooxml/scripts/unpack.py <office_file> <output_directory> 3. Create and run a Python script using the Document library (see "Document Library" section in ooxml.md) 4. Pack the final document: python ooxml/scripts/pack.py <input_directory> <office_file>
The Document library provides both high-level methods for common operations and direct DOM access for complex scenarios.
Redlining workflow for document review
This workflow allows you to plan comprehensive tracked changes using markdown before implementing them in OOXML. CRITICAL: For complete tracked changes, you must implement ALL changes systematically.
Batching Strategy: Group related changes into batches of 3-10 changes. This makes debugging manageable while maintaining efficiency. Test each batch before moving to the next.
Principle: Minimal, Precise Edits When implementing tracked changes, only mark text that actually changes. Repeating unchanged text makes edits harder to review and appears unprofessional. Break replacements into: [unchanged text] + [deletion] + [insertion] + [unchanged text]. Preserve the original run's RSID for unchanged text by extracting the <w:r> element from the original and reusing it.
Example - Changing "30 days" to "60 days" in a sentence:
# BAD - Replaces entire sentence
'<w:del><w:r><w:delText>The term is 30 days.</w:delText></w:r></w:del><w:ins><w:r><w:t>The term is 60 days.</w:t></w:r></w:ins>'
# GOOD - Only marks what changed, preserves original <w:r> for unchanged text
'<w:r w:rsidR="00AB12CD"><w:t>The term is </w:t></w:r><w:del><w:r><w:delText>30</w:delText></w:r></w:del><w:ins><w:r><w:t>60</w:t></w:r></w:ins><w:r w:rsidR="00AB12CD"><w:t> days.</w:t></w:r>'Tracked changes workflow
1. Get markdown representation: Convert document to markdown with tracked changes preserved:
pandoc --track-changes=all path-to-file.docx -o current.md2. Identify and group changes: Review the document and identify ALL changes needed, organizing them into logical batches:
Location methods (for finding changes in XML):
- Section/heading numbers (e.g., "Section 3.2", "Article IV")
- Paragraph identifiers if numbered
- Grep patterns with unique surrounding text
- Document structure (e.g., "first paragraph", "signature block")
- DO NOT use markdown line numbers - they don't map to XML structure
Batch organization (group 3-10 related changes per batch):
- By section: "Batch 1: Section 2 amendments", "Batch 2: Section 5 updates"
- By type: "Batch 1: Date corrections", "Batch 2: Party name changes"
- By complexity: Start with simple text replacements, then tackle complex structural changes
- Sequential: "Batch 1: Pages 1-3", "Batch 2: Pages 4-6"
3. Read documentation and unpack:
- MANDATORY - READ ENTIRE FILE: Read `ooxml.md` (~600 lines) completely from start to finish. NEVER set any range limits when reading this file. Pay special attention to the "Document Library" and "Tracked Change Patterns" sections.
- Unpack the document:
python ooxml/scripts/unpack.py <file.docx> <dir> - Note the suggested RSID: The unpack script will suggest an RSID to use for your tracked changes. Copy this RSID for use in step 4b.
4. Implement changes in batches: Group changes logically (by section, by type, or by proximity) and implement them together in a single script. This approach:
- Makes debugging easier (smaller batch = easier to isolate errors)
- Allows incremental progress
- Maintains efficiency (batch size of 3-10 changes works well)
Suggested batch groupings:
- By document section (e.g., "Section 3 changes", "Definitions", "Termination clause")
- By change type (e.g., "Date changes", "Party name updates", "Legal term replacements")
- By proximity (e.g., "Changes on pages 1-3", "Changes in first half of document")
For each batch of related changes:
a. Map text to XML: Grep for text in word/document.xml to verify how text is split across <w:r> elements.
b. Create and run script: Use get_node to find nodes, implement changes, then doc.save(). See "Document Library" section in ooxml.md for patterns.
Note: Always grep word/document.xml immediately before writing a script to get current line numbers and verify text content. Line numbers change after each script run.
5. Pack the document: After all batches are complete, convert the unpacked directory back to .docx:
python ooxml/scripts/pack.py unpacked reviewed-document.docx6. Final verification: Do a comprehensive check of the complete document:
- Convert final document to markdown:
pandoc --track-changes=all reviewed-document.docx -o verification.md- Verify ALL changes were applied correctly:
grep "original phrase" verification.md # Should NOT find it
grep "replacement phrase" verification.md # Should find it- Check that no unintended changes were introduced
Converting Documents to Images
To visually analyze Word documents, convert them to images using a two-step process:
1. Convert DOCX to PDF:
soffice --headless --convert-to pdf document.docx2. Convert PDF pages to JPEG images:
pdftoppm -jpeg -r 150 document.pdf pageThis creates files like page-1.jpg, page-2.jpg, etc.
Options:
-r 150: Sets resolution to 150 DPI (adjust for quality/size balance)-jpeg: Output JPEG format (use-pngfor PNG if preferred)-f N: First page to convert (e.g.,-f 2starts from page 2)-l N: Last page to convert (e.g.,-l 5stops at page 5)page: Prefix for output files
Example for specific range:
pdftoppm -jpeg -r 150 -f 2 -l 5 document.pdf page # Converts only pages 2-5Code Style Guidelines
IMPORTANT: When generating code for DOCX operations:
- Write concise code
- Avoid verbose variable names and redundant operations
- Avoid unnecessary print statements
Dependencies
Required dependencies (install if not available):
- pandoc:
sudo apt-get install pandoc(for text extraction) - docx:
npm install -g docx(for creating new documents) - LibreOffice:
sudo apt-get install libreoffice(for PDF conversion) - Poppler:
sudo apt-get install poppler-utils(for pdftoppm to convert PDF to images) - defusedxml:
pip install defusedxml(for secure XML parsing)
DOCX Library Tutorial
Generate .docx files with JavaScript/TypeScript.
Important: Read this entire document before starting. Critical formatting rules and common pitfalls are covered throughout - skipping sections may result in corrupted files or rendering issues.
Setup
Assumes docx is already installed globally If not installed: npm install -g docx
const {
Document,
Packer,
Paragraph,
TextRun,
Table,
TableRow,
TableCell,
ImageRun,
Media,
Header,
Footer,
AlignmentType,
PageOrientation,
LevelFormat,
ExternalHyperlink,
InternalHyperlink,
TableOfContents,
HeadingLevel,
BorderStyle,
WidthType,
TabStopType,
TabStopPosition,
UnderlineType,
ShadingType,
VerticalAlign,
SymbolRun,
PageNumber,
FootnoteReferenceRun,
Footnote,
PageBreak,
} = require("docx");
// Create & Save
const doc = new Document({
sections: [
{
children: [
/* content */
],
},
],
});
Packer.toBuffer(doc).then((buffer) => fs.writeFileSync("doc.docx", buffer)); // Node.js
Packer.toBlob(doc).then((blob) => {
/* download logic */
}); // BrowserText & Formatting
// IMPORTANT: Never use \n for line breaks - always use separate Paragraph elements
// ❌ WRONG: new TextRun("Line 1\nLine 2")
// ✅ CORRECT: new Paragraph({ children: [new TextRun("Line 1")] }), new Paragraph({ children: [new TextRun("Line 2")] })
// Basic text with all formatting options
new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { before: 200, after: 200 },
indent: { left: 720, right: 720 },
children: [
new TextRun({ text: "Bold", bold: true }),
new TextRun({ text: "Italic", italics: true }),
new TextRun({
text: "Underlined",
underline: { type: UnderlineType.DOUBLE, color: "FF0000" },
}),
new TextRun({ text: "Colored", color: "FF0000", size: 28, font: "Arial" }), // Arial default
new TextRun({ text: "Highlighted", highlight: "yellow" }),
new TextRun({ text: "Strikethrough", strike: true }),
new TextRun({ text: "x2", superScript: true }),
new TextRun({ text: "H2O", subScript: true }),
new TextRun({ text: "SMALL CAPS", smallCaps: true }),
new SymbolRun({ char: "2022", font: "Symbol" }), // Bullet •
new SymbolRun({ char: "00A9", font: "Arial" }), // Copyright © - Arial for symbols
],
});Styles & Professional Formatting
const doc = new Document({
styles: {
default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default
paragraphStyles: [
// Document title style - override built-in Title style
{
id: "Title",
name: "Title",
basedOn: "Normal",
run: { size: 56, bold: true, color: "000000", font: "Arial" },
paragraph: {
spacing: { before: 240, after: 120 },
alignment: AlignmentType.CENTER,
},
},
// IMPORTANT: Override built-in heading styles by using their exact IDs
{
id: "Heading1",
name: "Heading 1",
basedOn: "Normal",
next: "Normal",
quickFormat: true,
run: { size: 32, bold: true, color: "000000", font: "Arial" }, // 16pt
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 },
}, // Required for TOC
{
id: "Heading2",
name: "Heading 2",
basedOn: "Normal",
next: "Normal",
quickFormat: true,
run: { size: 28, bold: true, color: "000000", font: "Arial" }, // 14pt
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 },
},
// Custom styles use your own IDs
{
id: "myStyle",
name: "My Style",
basedOn: "Normal",
run: { size: 28, bold: true, color: "000000" },
paragraph: { spacing: { after: 120 }, alignment: AlignmentType.CENTER },
},
],
characterStyles: [
{
id: "myCharStyle",
name: "My Char Style",
run: {
color: "FF0000",
bold: true,
underline: { type: UnderlineType.SINGLE },
},
},
],
},
sections: [
{
properties: {
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } },
},
children: [
new Paragraph({
heading: HeadingLevel.TITLE,
children: [new TextRun("Document Title")],
}), // Uses overridden Title style
new Paragraph({
heading: HeadingLevel.HEADING_1,
children: [new TextRun("Heading 1")],
}), // Uses overridden Heading1 style
new Paragraph({
style: "myStyle",
children: [new TextRun("Custom paragraph style")],
}),
new Paragraph({
children: [
new TextRun("Normal with "),
new TextRun({ text: "custom char style", style: "myCharStyle" }),
],
}),
],
},
],
});Professional Font Combinations:
- Arial (Headers) + Arial (Body) - Most universally supported, clean and professional
- Times New Roman (Headers) + Arial (Body) - Classic serif headers with modern sans-serif body
- Georgia (Headers) + Verdana (Body) - Optimized for screen reading, elegant contrast
Key Styling Principles:
- Override built-in styles: Use exact IDs like "Heading1", "Heading2", "Heading3" to override Word's built-in heading styles
- HeadingLevel constants:
HeadingLevel.HEADING_1uses "Heading1" style,HeadingLevel.HEADING_2uses "Heading2" style, etc. - Include outlineLevel: Set
outlineLevel: 0for H1,outlineLevel: 1for H2, etc. to ensure TOC works correctly - Use custom styles instead of inline formatting for consistency
- Set a default font using
styles.default.document.run.font- Arial is universally supported - Establish visual hierarchy with different font sizes (titles > headers > body)
- Add proper spacing with
beforeandafterparagraph spacing - Use colors sparingly: Default to black (000000) and shades of gray for titles and headings (heading 1, heading 2, etc.)
- Set consistent margins (1440 = 1 inch is standard)
Lists (ALWAYS USE PROPER LISTS - NEVER USE UNICODE BULLETS)
// Bullets - ALWAYS use the numbering config, NOT unicode symbols
// CRITICAL: Use LevelFormat.BULLET constant, NOT the string "bullet"
const doc = new Document({
numbering: {
config: [
{
reference: "bullet-list",
levels: [
{
level: 0,
format: LevelFormat.BULLET,
text: "•",
alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } },
},
],
},
{
reference: "first-numbered-list",
levels: [
{
level: 0,
format: LevelFormat.DECIMAL,
text: "%1.",
alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } },
},
],
},
{
reference: "second-numbered-list", // Different reference = restarts at 1
levels: [
{
level: 0,
format: LevelFormat.DECIMAL,
text: "%1.",
alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } },
},
],
},
],
},
sections: [
{
children: [
// Bullet list items
new Paragraph({
numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun("First bullet point")],
}),
new Paragraph({
numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun("Second bullet point")],
}),
// Numbered list items
new Paragraph({
numbering: { reference: "first-numbered-list", level: 0 },
children: [new TextRun("First numbered item")],
}),
new Paragraph({
numbering: { reference: "first-numbered-list", level: 0 },
children: [new TextRun("Second numbered item")],
}),
// ⚠️ CRITICAL: Different reference = INDEPENDENT list that restarts at 1
// Same reference = CONTINUES previous numbering
new Paragraph({
numbering: { reference: "second-numbered-list", level: 0 },
children: [
new TextRun("Starts at 1 again (because different reference)"),
],
}),
],
},
],
});
// ⚠️ CRITICAL NUMBERING RULE: Each reference creates an INDEPENDENT numbered list
// - Same reference = continues numbering (1, 2, 3... then 4, 5, 6...)
// - Different reference = restarts at 1 (1, 2, 3... then 1, 2, 3...)
// Use unique reference names for each separate numbered section!
// ⚠️ CRITICAL: NEVER use unicode bullets - they create fake lists that don't work properly
// new TextRun("• Item") // WRONG
// new SymbolRun({ char: "2022" }) // WRONG
// ✅ ALWAYS use numbering config with LevelFormat.BULLET for real Word listsTables
// Complete table with margins, borders, headers, and bullet points
const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const cellBorders = {
top: tableBorder,
bottom: tableBorder,
left: tableBorder,
right: tableBorder,
};
new Table({
columnWidths: [4680, 4680], // ⚠️ CRITICAL: Set column widths at table level - values in DXA (twentieths of a point)
margins: { top: 100, bottom: 100, left: 180, right: 180 }, // Set once for all cells
rows: [
new TableRow({
tableHeader: true,
children: [
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell
// ⚠️ CRITICAL: Always use ShadingType.CLEAR to prevent black backgrounds in Word.
shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
verticalAlign: VerticalAlign.CENTER,
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "Header", bold: true, size: 22 })],
}),
],
}),
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell
shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({ text: "Bullet Points", bold: true, size: 22 }),
],
}),
],
}),
],
}),
new TableRow({
children: [
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell
children: [
new Paragraph({ children: [new TextRun("Regular data")] }),
],
}),
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell
children: [
new Paragraph({
numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun("First bullet point")],
}),
new Paragraph({
numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun("Second bullet point")],
}),
],
}),
],
}),
],
});IMPORTANT: Table Width & Borders
- Use BOTH
columnWidths: [width1, width2, ...]array ANDwidth: { size: X, type: WidthType.DXA }on each cell - Values in DXA (twentieths of a point): 1440 = 1 inch, Letter usable width = 9360 DXA (with 1" margins)
- Apply borders to individual
TableCellelements, NOT theTableitself
Precomputed Column Widths (Letter size with 1" margins = 9360 DXA total):
- 2 columns:
columnWidths: [4680, 4680](equal width) - 3 columns:
columnWidths: [3120, 3120, 3120](equal width)
Links & Navigation
// TOC (requires headings) - CRITICAL: Use HeadingLevel only, NOT custom styles
// ❌ WRONG: new Paragraph({ heading: HeadingLevel.HEADING_1, style: "customHeader", children: [new TextRun("Title")] })
// ✅ CORRECT: new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] })
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" }),
// External link
new Paragraph({
children: [new ExternalHyperlink({
children: [new TextRun({ text: "Google", style: "Hyperlink" })],
link: "https://www.google.com"
})]
}),
// Internal link & bookmark
new Paragraph({
children: [new InternalHyperlink({
children: [new TextRun({ text: "Go to Section", style: "Hyperlink" })],
anchor: "section1"
})]
}),
new Paragraph({
children: [new TextRun("Section Content")],
bookmark: { id: "section1", name: "section1" }
}),Images & Media
// Basic image with sizing & positioning
// CRITICAL: Always specify 'type' parameter - it's REQUIRED for ImageRun
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new ImageRun({
type: "png", // NEW REQUIREMENT: Must specify image type (png, jpg, jpeg, gif, bmp, svg)
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150, rotation: 0 }, // rotation in degrees
altText: { title: "Logo", description: "Company logo", name: "Name" }, // IMPORTANT: All three fields are required
}),
],
});Page Breaks
// Manual page break
(new Paragraph({ children: [new PageBreak()] }),
// Page break before paragraph
new Paragraph({
pageBreakBefore: true,
children: [new TextRun("This starts on a new page")],
}));
// ⚠️ CRITICAL: NEVER use PageBreak standalone - it will create invalid XML that Word cannot open
// ❌ WRONG: new PageBreak()
// ✅ CORRECT: new Paragraph({ children: [new PageBreak()] })Headers/Footers & Page Setup
const doc = new Document({
sections: [
{
properties: {
page: {
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1440 = 1 inch
size: { orientation: PageOrientation.LANDSCAPE },
pageNumbers: { start: 1, formatType: "decimal" }, // "upperRoman", "lowerRoman", "upperLetter", "lowerLetter"
},
},
headers: {
default: new Header({
children: [
new Paragraph({
alignment: AlignmentType.RIGHT,
children: [new TextRun("Header Text")],
}),
],
}),
},
footers: {
default: new Footer({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun("Page "),
new TextRun({ children: [PageNumber.CURRENT] }),
new TextRun(" of "),
new TextRun({ children: [PageNumber.TOTAL_PAGES] }),
],
}),
],
}),
},
children: [
/* content */
],
},
],
});Tabs
new Paragraph({
tabStops: [
{ type: TabStopType.LEFT, position: TabStopPosition.MAX / 4 },
{ type: TabStopType.CENTER, position: TabStopPosition.MAX / 2 },
{ type: TabStopType.RIGHT, position: (TabStopPosition.MAX * 3) / 4 },
],
children: [new TextRun("Left\tCenter\tRight")],
});Constants & Quick Reference
- Underlines:
SINGLE,DOUBLE,WAVY,DASH - Borders:
SINGLE,DOUBLE,DASHED,DOTTED - Numbering:
DECIMAL(1,2,3),UPPER_ROMAN(I,II,III),LOWER_LETTER(a,b,c) - Tabs:
LEFT,CENTER,RIGHT,DECIMAL - Symbols:
"2022"(•),"00A9"(©),"00AE"(®),"2122"(™),"00B0"(°),"F070"(✓),"F0FC"(✗)
Critical Issues & Common Mistakes
- CRITICAL: PageBreak must ALWAYS be inside a Paragraph - standalone PageBreak creates invalid XML that Word cannot open
- ALWAYS use ShadingType.CLEAR for table cell shading - Never use ShadingType.SOLID (causes black background).
- Measurements in DXA (1440 = 1 inch) | Each table cell needs ≥1 Paragraph | TOC requires HeadingLevel styles only
- ALWAYS use custom styles with Arial font for professional appearance and proper visual hierarchy
- ALWAYS set a default font using
styles.default.document.run.font- Arial recommended - ALWAYS use columnWidths array for tables + individual cell widths for compatibility
- NEVER use unicode symbols for bullets - always use proper numbering configuration with
LevelFormat.BULLETconstant (NOT the string "bullet") - NEVER use \n for line breaks anywhere - always use separate Paragraph elements for each line
- ALWAYS use TextRun objects within Paragraph children - never use text property directly on Paragraph
- CRITICAL for images: ImageRun REQUIRES
typeparameter - always specify "png", "jpg", "jpeg", "gif", "bmp", or "svg" - CRITICAL for bullets: Must use
LevelFormat.BULLETconstant, not string "bullet", and includetext: "•"for the bullet character - CRITICAL for numbering: Each numbering reference creates an INDEPENDENT list. Same reference = continues numbering (1,2,3 then 4,5,6). Different reference = restarts at 1 (1,2,3 then 1,2,3). Use unique reference names for each separate numbered section!
- CRITICAL for TOC: When using TableOfContents, headings must use HeadingLevel ONLY - do NOT add custom styles to heading paragraphs or TOC will break
- Tables: Set
columnWidthsarray + individual cell widths, apply borders to cells not table - Set table margins at TABLE level for consistent cell padding (avoids repetition per cell)
© 2025 Anthropic, PBC. All rights reserved.
LICENSE: Use of these materials (including all code, prompts, assets, files,
and other components of this Skill) is governed by your agreement with
Anthropic regarding use of Anthropic's services. If no separate agreement
exists, use is governed by Anthropic's Consumer Terms of Service or
Commercial Terms of Service, as applicable:
https://www.anthropic.com/legal/consumer-terms
https://www.anthropic.com/legal/commercial-terms
Your applicable agreement is referred to as the "Agreement." "Services" are
as defined in the Agreement.
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
contrary, users may not:
- Extract these materials from the Services or retain copies of these
materials outside the Services
- Reproduce or copy these materials, except for temporary copies created
automatically during authorized use of the Services
- Create derivative works based on these materials
- Distribute, sublicense, or transfer these materials to any third party
- Make, offer to sell, sell, or import any inventions embodied in these
materials
- Reverse engineer, decompile, or disassemble these materials
The receipt, viewing, or possession of these materials does not convey or
imply any license or right beyond those expressly granted above.
Anthropic retains all right, title, and interest in these materials,
including all copyrights, patents, and other intellectual property rights.
Office Open XML Technical Reference
Important: Read this entire document before starting. This document covers:
- Technical Guidelines - Schema compliance rules and validation requirements
- Document Content Patterns - XML patterns for headings, lists, tables, formatting, etc.
- Document Library (Python) - Recommended approach for OOXML manipulation with automatic infrastructure setup
- Tracked Changes (Redlining) - XML patterns for implementing tracked changes
Technical Guidelines
Schema Compliance
- Element ordering in `<w:pPr>`:
<w:pStyle>,<w:numPr>,<w:spacing>,<w:ind>,<w:jc> - Whitespace: Add
xml:space='preserve'to<w:t>elements with leading/trailing spaces - Unicode: Escape characters in ASCII content:
"becomes“ - Character encoding reference: Curly quotes
""become“”, apostrophe'becomes’, em-dash—becomes— - Tracked changes: Use
<w:del>and<w:ins>tags withw:author="Claude"outside<w:r>elements - Critical:
<w:ins>closes with</w:ins>,<w:del>closes with</w:del>- never mix - RSIDs must be 8-digit hex: Use values like
00AB1234(only 0-9, A-F characters) - trackRevisions placement: Add
<w:trackRevisions/>after<w:proofState>in settings.xml - Images: Add to
word/media/, reference indocument.xml, set dimensions to prevent overflow
Document Content Patterns
Basic Structure
<w:p>
<w:r><w:t>Text content</w:t></w:r>
</w:p>Headings and Styles
<w:p>
<w:pPr>
<w:pStyle w:val="Title"/>
<w:jc w:val="center"/>
</w:pPr>
<w:r><w:t>Document Title</w:t></w:r>
</w:p>
<w:p>
<w:pPr><w:pStyle w:val="Heading2"/></w:pPr>
<w:r><w:t>Section Heading</w:t></w:r>
</w:p>Text Formatting
<!-- Bold -->
<w:r><w:rPr><w:b/><w:bCs/></w:rPr><w:t>Bold</w:t></w:r>
<!-- Italic -->
<w:r><w:rPr><w:i/><w:iCs/></w:rPr><w:t>Italic</w:t></w:r>
<!-- Underline -->
<w:r><w:rPr><w:u w:val="single"/></w:rPr><w:t>Underlined</w:t></w:r>
<!-- Highlight -->
<w:r><w:rPr><w:highlight w:val="yellow"/></w:rPr><w:t>Highlighted</w:t></w:r>Lists
<!-- Numbered list -->
<w:p>
<w:pPr>
<w:pStyle w:val="ListParagraph"/>
<w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr>
<w:spacing w:before="240"/>
</w:pPr>
<w:r><w:t>First item</w:t></w:r>
</w:p>
<!-- Restart numbered list at 1 - use different numId -->
<w:p>
<w:pPr>
<w:pStyle w:val="ListParagraph"/>
<w:numPr><w:ilvl w:val="0"/><w:numId w:val="2"/></w:numPr>
<w:spacing w:before="240"/>
</w:pPr>
<w:r><w:t>New list item 1</w:t></w:r>
</w:p>
<!-- Bullet list (level 2) -->
<w:p>
<w:pPr>
<w:pStyle w:val="ListParagraph"/>
<w:numPr><w:ilvl w:val="1"/><w:numId w:val="1"/></w:numPr>
<w:spacing w:before="240"/>
<w:ind w:left="900"/>
</w:pPr>
<w:r><w:t>Bullet item</w:t></w:r>
</w:p>Tables
<w:tbl>
<w:tblPr>
<w:tblStyle w:val="TableGrid"/>
<w:tblW w:w="0" w:type="auto"/>
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="4675"/><w:gridCol w:w="4675"/>
</w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr><w:tcW w:w="4675" w:type="dxa"/></w:tcPr>
<w:p><w:r><w:t>Cell 1</w:t></w:r></w:p>
</w:tc>
<w:tc>
<w:tcPr><w:tcW w:w="4675" w:type="dxa"/></w:tcPr>
<w:p><w:r><w:t>Cell 2</w:t></w:r></w:p>
</w:tc>
</w:tr>
</w:tbl>Layout
<!-- Page break before new section (common pattern) -->
<w:p>
<w:r>
<w:br w:type="page"/>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Heading1"/>
</w:pPr>
<w:r>
<w:t>New Section Title</w:t>
</w:r>
</w:p>
<!-- Centered paragraph -->
<w:p>
<w:pPr>
<w:spacing w:before="240" w:after="0"/>
<w:jc w:val="center"/>
</w:pPr>
<w:r><w:t>Centered text</w:t></w:r>
</w:p>
<!-- Font change - paragraph level (applies to all runs) -->
<w:p>
<w:pPr>
<w:rPr><w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/></w:rPr>
</w:pPr>
<w:r><w:t>Monospace text</w:t></w:r>
</w:p>
<!-- Font change - run level (specific to this text) -->
<w:p>
<w:r>
<w:rPr><w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/></w:rPr>
<w:t>This text is Courier New</w:t>
</w:r>
<w:r><w:t> and this text uses default font</w:t></w:r>
</w:p>File Updates
When adding content, update these files:
`word/_rels/document.xml.rels`:
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>`[Content_Types].xml`:
<Default Extension="png" ContentType="image/png"/>
<Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>Images
CRITICAL: Calculate dimensions to prevent page overflow and maintain aspect ratio.
<!-- Minimal required structure -->
<w:p>
<w:r>
<w:drawing>
<wp:inline>
<wp:extent cx="2743200" cy="1828800"/>
<wp:docPr id="1" name="Picture 1"/>
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:nvPicPr>
<pic:cNvPr id="0" name="image1.png"/>
<pic:cNvPicPr/>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="rId5"/>
<!-- Add for stretch fill with aspect ratio preservation -->
<a:stretch>
<a:fillRect/>
</a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:ext cx="2743200" cy="1828800"/>
</a:xfrm>
<a:prstGeom prst="rect"/>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
</w:p>Links (Hyperlinks)
IMPORTANT: All hyperlinks (both internal and external) require the Hyperlink style to be defined in styles.xml. Without this style, links will look like regular text instead of blue underlined clickable links.
External Links:
<!-- In document.xml -->
<w:hyperlink r:id="rId5">
<w:r>
<w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>
<w:t>Link Text</w:t>
</w:r>
</w:hyperlink>
<!-- In word/_rels/document.xml.rels -->
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
Target="https://www.example.com/" TargetMode="External"/>Internal Links:
<!-- Link to bookmark -->
<w:hyperlink w:anchor="myBookmark">
<w:r>
<w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>
<w:t>Link Text</w:t>
</w:r>
</w:hyperlink>
<!-- Bookmark target -->
<w:bookmarkStart w:id="0" w:name="myBookmark"/>
<w:r><w:t>Target content</w:t></w:r>
<w:bookmarkEnd w:id="0"/>Hyperlink Style (required in styles.xml):
<w:style w:type="character" w:styleId="Hyperlink">
<w:name w:val="Hyperlink"/>
<w:basedOn w:val="DefaultParagraphFont"/>
<w:uiPriority w:val="99"/>
<w:unhideWhenUsed/>
<w:rPr>
<w:color w:val="467886" w:themeColor="hyperlink"/>
<w:u w:val="single"/>
</w:rPr>
</w:style>Document Library (Python)
Use the Document class from scripts/document.py for all tracked changes and comments. It automatically handles infrastructure setup (people.xml, RSIDs, settings.xml, comment files, relationships, content types). Only use direct XML manipulation for complex scenarios not supported by the library.
Working with Unicode and Entities:
- Searching: Both entity notation and Unicode characters work -
contains="“Company"andcontains="\u201cCompany"find the same text - Replacing: Use either entities (
“) or Unicode (\u201c) - both work and will be converted appropriately based on the file's encoding (ascii → entities, utf-8 → Unicode)
Initialization
Find the docx skill root (directory containing scripts/ and ooxml/):
# Search for document.py to locate the skill root
# Note: /mnt/skills is used here as an example; check your context for the actual location
find /mnt/skills -name "document.py" -path "*/docx/scripts/*" 2>/dev/null | head -1
# Example output: /mnt/skills/docx/scripts/document.py
# Skill root is: /mnt/skills/docxRun your script with PYTHONPATH set to the docx skill root:
PYTHONPATH=/mnt/skills/docx python your_script.pyIn your script, import from the skill root:
from scripts.document import Document, DocxXMLEditor
# Basic initialization (automatically creates temp copy and sets up infrastructure)
doc = Document('unpacked')
# Customize author and initials
doc = Document('unpacked', author="John Doe", initials="JD")
# Enable track revisions mode
doc = Document('unpacked', track_revisions=True)
# Specify custom RSID (auto-generated if not provided)
doc = Document('unpacked', rsid="07DC5ECB")Creating Tracked Changes
CRITICAL: Only mark text that actually changes. Keep ALL unchanged text outside <w:del>/<w:ins> tags. Marking unchanged text makes edits unprofessional and harder to review.
Attribute Handling: The Document class auto-injects attributes (w:id, w:date, w:rsidR, w:rsidDel, w16du:dateUtc, xml:space) into new elements. When preserving unchanged text from the original document, copy the original <w:r> element with its existing attributes to maintain document integrity.
Method Selection Guide:
- Adding your own changes to regular text: Use
replace_node()with<w:del>/<w:ins>tags, orsuggest_deletion()for removing entire<w:r>or<w:p>elements - Partially modifying another author's tracked change: Use
replace_node()to nest your changes inside their<w:ins>/<w:del> - Completely rejecting another author's insertion: Use
revert_insertion()on the<w:ins>element (NOTsuggest_deletion()) - Completely rejecting another author's deletion: Use
revert_deletion()on the<w:del>element to restore deleted content using tracked changes
# Minimal edit - change one word: "The report is monthly" → "The report is quarterly"
# Original: <w:r w:rsidR="00AB12CD"><w:rPr><w:rFonts w:ascii="Calibri"/></w:rPr><w:t>The report is monthly</w:t></w:r>
node = doc["word/document.xml"].get_node(tag="w:r", contains="The report is monthly")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:r w:rsidR="00AB12CD">{rpr}<w:t>The report is </w:t></w:r><w:del><w:r>{rpr}<w:delText>monthly</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>quarterly</w:t></w:r></w:ins>'
doc["word/document.xml"].replace_node(node, replacement)
# Minimal edit - change number: "within 30 days" → "within 45 days"
# Original: <w:r w:rsidR="00XYZ789"><w:rPr><w:rFonts w:ascii="Calibri"/></w:rPr><w:t>within 30 days</w:t></w:r>
node = doc["word/document.xml"].get_node(tag="w:r", contains="within 30 days")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:r w:rsidR="00XYZ789">{rpr}<w:t>within </w:t></w:r><w:del><w:r>{rpr}<w:delText>30</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>45</w:t></w:r></w:ins><w:r w:rsidR="00XYZ789">{rpr}<w:t> days</w:t></w:r>'
doc["word/document.xml"].replace_node(node, replacement)
# Complete replacement - preserve formatting even when replacing all text
node = doc["word/document.xml"].get_node(tag="w:r", contains="apple")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:del><w:r>{rpr}<w:delText>apple</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>banana orange</w:t></w:r></w:ins>'
doc["word/document.xml"].replace_node(node, replacement)
# Insert new content (no attributes needed - auto-injected)
node = doc["word/document.xml"].get_node(tag="w:r", contains="existing text")
doc["word/document.xml"].insert_after(node, '<w:ins><w:r><w:t>new text</w:t></w:r></w:ins>')
# Partially delete another author's insertion
# Original: <w:ins w:author="Jane Smith" w:date="..."><w:r><w:t>quarterly financial report</w:t></w:r></w:ins>
# Goal: Delete only "financial" to make it "quarterly report"
node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"})
# IMPORTANT: Preserve w:author="Jane Smith" on the outer <w:ins> to maintain authorship
replacement = '''<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
<w:r><w:t>quarterly </w:t></w:r>
<w:del><w:r><w:delText>financial </w:delText></w:r></w:del>
<w:r><w:t>report</w:t></w:r>
</w:ins>'''
doc["word/document.xml"].replace_node(node, replacement)
# Change part of another author's insertion
# Original: <w:ins w:author="Jane Smith"><w:r><w:t>in silence, safe and sound</w:t></w:r></w:ins>
# Goal: Change "safe and sound" to "soft and unbound"
node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "8"})
replacement = f'''<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
<w:r><w:t>in silence, </w:t></w:r>
</w:ins>
<w:ins>
<w:r><w:t>soft and unbound</w:t></w:r>
</w:ins>
<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
<w:del><w:r><w:delText>safe and sound</w:delText></w:r></w:del>
</w:ins>'''
doc["word/document.xml"].replace_node(node, replacement)
# Delete entire run (use only when deleting all content; use replace_node for partial deletions)
node = doc["word/document.xml"].get_node(tag="w:r", contains="text to delete")
doc["word/document.xml"].suggest_deletion(node)
# Delete entire paragraph (in-place, handles both regular and numbered list paragraphs)
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph to delete")
doc["word/document.xml"].suggest_deletion(para)
# Add new numbered list item
target_para = doc["word/document.xml"].get_node(tag="w:p", contains="existing list item")
pPr = tags[0].toxml() if (tags := target_para.getElementsByTagName("w:pPr")) else ""
new_item = f'<w:p>{pPr}<w:r><w:t>New item</w:t></w:r></w:p>'
tracked_para = DocxXMLEditor.suggest_paragraph(new_item)
doc["word/document.xml"].insert_after(target_para, tracked_para)
# Optional: add spacing paragraph before content for better visual separation
# spacing = DocxXMLEditor.suggest_paragraph('<w:p><w:pPr><w:pStyle w:val="ListParagraph"/></w:pPr></w:p>')
# doc["word/document.xml"].insert_after(target_para, spacing + tracked_para)Adding Comments
# Add comment spanning two existing tracked changes
# Note: w:id is auto-generated. Only search by w:id if you know it from XML inspection
start_node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"})
end_node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "2"})
doc.add_comment(start=start_node, end=end_node, text="Explanation of this change")
# Add comment on a paragraph
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
doc.add_comment(start=para, end=para, text="Comment on this paragraph")
# Add comment on newly created tracked change
# First create the tracked change
node = doc["word/document.xml"].get_node(tag="w:r", contains="old")
new_nodes = doc["word/document.xml"].replace_node(
node,
'<w:del><w:r><w:delText>old</w:delText></w:r></w:del><w:ins><w:r><w:t>new</w:t></w:r></w:ins>'
)
# Then add comment on the newly created elements
# new_nodes[0] is the <w:del>, new_nodes[1] is the <w:ins>
doc.add_comment(start=new_nodes[0], end=new_nodes[1], text="Changed old to new per requirements")
# Reply to existing comment
doc.reply_to_comment(parent_comment_id=0, text="I agree with this change")Rejecting Tracked Changes
IMPORTANT: Use revert_insertion() to reject insertions and revert_deletion() to restore deletions using tracked changes. Use suggest_deletion() only for regular unmarked content.
# Reject insertion (wraps it in deletion)
# Use this when another author inserted text that you want to delete
ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"})
nodes = doc["word/document.xml"].revert_insertion(ins) # Returns [ins]
# Reject deletion (creates insertion to restore deleted content)
# Use this when another author deleted text that you want to restore
del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"})
nodes = doc["word/document.xml"].revert_deletion(del_elem) # Returns [del_elem, new_ins]
# Reject all insertions in a paragraph
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
nodes = doc["word/document.xml"].revert_insertion(para) # Returns [para]
# Reject all deletions in a paragraph
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
nodes = doc["word/document.xml"].revert_deletion(para) # Returns [para]Inserting Images
CRITICAL: The Document class works with a temporary copy at doc.unpacked_path. Always copy images to this temp directory, not the original unpacked folder.
from PIL import Image
import shutil, os
# Initialize document first
doc = Document('unpacked')
# Copy image and calculate full-width dimensions with aspect ratio
media_dir = os.path.join(doc.unpacked_path, 'word/media')
os.makedirs(media_dir, exist_ok=True)
shutil.copy('image.png', os.path.join(media_dir, 'image1.png'))
img = Image.open(os.path.join(media_dir, 'image1.png'))
width_emus = int(6.5 * 914400) # 6.5" usable width, 914400 EMUs/inch
height_emus = int(width_emus * img.size[1] / img.size[0])
# Add relationship and content type
rels_editor = doc['word/_rels/document.xml.rels']
next_rid = rels_editor.get_next_rid()
rels_editor.append_to(rels_editor.dom.documentElement,
f'<Relationship Id="{next_rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>')
doc['[Content_Types].xml'].append_to(doc['[Content_Types].xml'].dom.documentElement,
'<Default Extension="png" ContentType="image/png"/>')
# Insert image
node = doc["word/document.xml"].get_node(tag="w:p", line_number=100)
doc["word/document.xml"].insert_after(node, f'''<w:p>
<w:r>
<w:drawing>
<wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="{width_emus}" cy="{height_emus}"/>
<wp:docPr id="1" name="Picture 1"/>
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:nvPicPr><pic:cNvPr id="1" name="image1.png"/><pic:cNvPicPr/></pic:nvPicPr>
<pic:blipFill><a:blip r:embed="{next_rid}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
<pic:spPr><a:xfrm><a:ext cx="{width_emus}" cy="{height_emus}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
</w:p>''')Getting Nodes
# By text content
node = doc["word/document.xml"].get_node(tag="w:p", contains="specific text")
# By line range
para = doc["word/document.xml"].get_node(tag="w:p", line_number=range(100, 150))
# By attributes
node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"})
# By exact line number (must be line number where tag opens)
para = doc["word/document.xml"].get_node(tag="w:p", line_number=42)
# Combine filters
node = doc["word/document.xml"].get_node(tag="w:r", line_number=range(40, 60), contains="text")
# Disambiguate when text appears multiple times - add line_number range
node = doc["word/document.xml"].get_node(tag="w:r", contains="Section", line_number=range(2400, 2500))Saving
# Save with automatic validation (copies back to original directory)
doc.save() # Validates by default, raises error if validation fails
# Save to different location
doc.save('modified-unpacked')
# Skip validation (debugging only - needing this in production indicates XML issues)
doc.save(validate=False)Direct DOM Manipulation
For complex scenarios not covered by the library:
# Access any XML file
editor = doc["word/document.xml"]
editor = doc["word/comments.xml"]
# Direct DOM access (defusedxml.minidom.Document)
node = doc["word/document.xml"].get_node(tag="w:p", line_number=5)
parent = node.parentNode
parent.removeChild(node)
parent.appendChild(node) # Move to end
# General document manipulation (without tracked changes)
old_node = doc["word/document.xml"].get_node(tag="w:p", contains="original text")
doc["word/document.xml"].replace_node(old_node, "<w:p><w:r><w:t>replacement text</w:t></w:r></w:p>")
# Multiple insertions - use return value to maintain order
node = doc["word/document.xml"].get_node(tag="w:r", line_number=100)
nodes = doc["word/document.xml"].insert_after(node, "<w:r><w:t>A</w:t></w:r>")
nodes = doc["word/document.xml"].insert_after(nodes[-1], "<w:r><w:t>B</w:t></w:r>")
nodes = doc["word/document.xml"].insert_after(nodes[-1], "<w:r><w:t>C</w:t></w:r>")
# Results in: original_node, A, B, CTracked Changes (Redlining)
Use the Document class above for all tracked changes. The patterns below are for reference when constructing replacement XML strings.
Validation Rules
The validator checks that the document text matches the original after reverting Claude's changes. This means:
- NEVER modify text inside another author's `<w:ins>` or `<w:del>` tags
- ALWAYS use nested deletions to remove another author's insertions
- Every edit must be properly tracked with
<w:ins>or<w:del>tags
Tracked Change Patterns
CRITICAL RULES:
1. Never modify the content inside another author's tracked changes. Always use nested deletions. 2. XML Structure: Always place <w:del> and <w:ins> at paragraph level containing complete <w:r> elements. Never nest inside <w:r> elements - this creates invalid XML that breaks document processing.
Text Insertion:
<w:ins w:id="1" w:author="Claude" w:date="2025-07-30T23:05:00Z" w16du:dateUtc="2025-07-31T06:05:00Z">
<w:r w:rsidR="00792858">
<w:t>inserted text</w:t>
</w:r>
</w:ins>Text Deletion:
<w:del w:id="2" w:author="Claude" w:date="2025-07-30T23:05:00Z" w16du:dateUtc="2025-07-31T06:05:00Z">
<w:r w:rsidDel="00792858">
<w:delText>deleted text</w:delText>
</w:r>
</w:del>Deleting Another Author's Insertion (MUST use nested structure):
<!-- Nest deletion inside the original insertion -->
<w:ins w:author="Jane Smith" w:id="16">
<w:del w:author="Claude" w:id="40">
<w:r><w:delText>monthly</w:delText></w:r>
</w:del>
</w:ins>
<w:ins w:author="Claude" w:id="41">
<w:r><w:t>weekly</w:t></w:r>
</w:ins>Restoring Another Author's Deletion:
<!-- Leave their deletion unchanged, add new insertion after it -->
<w:del w:author="Jane Smith" w:id="50">
<w:r><w:delText>within 30 days</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="51">
<w:r><w:t>within 30 days</w:t></w:r>
</w:ins>export default async function document_skills(input) {
console.log("🧠 Running skill: document-skills");
// TODO: implement actual logic for this skill
return {
message: "Skill 'document-skills' executed successfully!",
input,
};
}
{
"name": "@ai-labs-claude-skills/document-skills",
"version": "1.0.0",
"description": "Claude AI skill: document-skills",
"main": "index.js",
"files": [
"."
],
"license": "MIT",
"author": "AI Labs"
}
CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.
If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory: python scripts/check_fillable_fields <file.pdf>, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions.
Fillable fields
If the PDF has fillable form fields:
- Run this script from this file's directory:
python scripts/extract_form_field_info.py <input.pdf> <field_info.json>. It will create a JSON file with a list of fields in this format:
[
{
"field_id": (unique ID for the field),
"page": (page number, 1-based),
"rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page),
"type": ("text", "checkbox", "radio_group", or "choice"),
},
// Checkboxes have "checked_value" and "unchecked_value" properties:
{
"field_id": (unique ID for the field),
"page": (page number, 1-based),
"type": "checkbox",
"checked_value": (Set the field to this value to check the checkbox),
"unchecked_value": (Set the field to this value to uncheck the checkbox),
},
// Radio groups have a "radio_options" list with the possible choices.
{
"field_id": (unique ID for the field),
"page": (page number, 1-based),
"type": "radio_group",
"radio_options": [
{
"value": (set the field to this value to select this radio option),
"rect": (bounding box for the radio button for this option)
},
// Other radio options
]
},
// Multiple choice fields have a "choice_options" list with the possible choices:
{
"field_id": (unique ID for the field),
"page": (page number, 1-based),
"type": "choice",
"choice_options": [
{
"value": (set the field to this value to select this option),
"text": (display text of the option)
},
// Other choice options
],
}
]- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory):
python scripts/convert_pdf_to_images.py <file.pdf> <output_directory> Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates).
- Create a
field_values.jsonfile in this format with the values to be entered for each field:
[
{
"field_id": "last_name", // Must match the field_id from `extract_form_field_info.py`
"description": "The user's last name",
"page": 1, // Must match the "page" value in field_info.json
"value": "Simpson"
},
{
"field_id": "Checkbox12",
"description": "Checkbox to be checked if the user is 18 or over",
"page": 1,
"value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options".
},
// more fields
]- Run the
fill_fillable_fields.pyscript from this file's directory to create a filled-in PDF:
python scripts/fill_fillable_fields.py <input pdf> <field_values.json> <output pdf> This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again.
Non-fillable fields
If the PDF doesn't have fillable form fields, you'll need to visually determine where the data should be added and create text annotations. Follow the below steps _exactly_. You MUST perform all of these steps to ensure that the the form is accurately completed. Details for each step are below.
- Convert the PDF to PNG images and determine field bounding boxes.
- Create a JSON file with field information and validation images showing the bounding boxes.
- Validate the the bounding boxes.
- Use the bounding boxes to fill in the form.
Step 1: Visual Analysis (REQUIRED)
- Convert the PDF to PNG images. Run this script from this file's directory:
python scripts/convert_pdf_to_images.py <file.pdf> <output_directory> The script will create a PNG image for each page in the PDF.
- Carefully examine each PNG image and identify all form fields and areas where the user should enter data. For each form field where the user should enter text, determine bounding boxes for both the form field label, and the area where the user should enter text. The label and entry bounding boxes MUST NOT INTERSECT; the text entry box should only include the area where data should be entered. Usually this area will be immediately to the side, above, or below its label. Entry bounding boxes must be tall and wide enough to contain their text.
These are some examples of form structures that you might see:
_Label inside box_
┌────────────────────────┐
│ Name: │
└────────────────────────┘The input area should be to the right of the "Name" label and extend to the edge of the box.
_Label before line_
Email: _______________________The input area should be above the line and include its entire width.
_Label under line_
_________________________
NameThe input area should be above the line and include the entire width of the line. This is common for signature and date fields.
_Label above line_
Please enter any special requests:
________________________________________________The input area should extend from the bottom of the label to the line, and should include the entire width of the line.
_Checkboxes_
Are you a US citizen? Yes □ No □For checkboxes:
- Look for small square boxes (□) - these are the actual checkboxes to target. They may be to the left or right of their labels.
- Distinguish between label text ("Yes", "No") and the clickable checkbox squares.
- The entry bounding box should cover ONLY the small square, not the text label.
Step 2: Create fields.json and validation images (REQUIRED)
- Create a file named
fields.jsonwith information for the form fields and bounding boxes in this format:
{
"pages": [
{
"page_number": 1,
"image_width": (first page image width in pixels),
"image_height": (first page image height in pixels),
},
{
"page_number": 2,
"image_width": (second page image width in pixels),
"image_height": (second page image height in pixels),
}
// additional pages
],
"form_fields": [
// Example for a text field.
{
"page_number": 1,
"description": "The user's last name should be entered here",
// Bounding boxes are [left, top, right, bottom]. The bounding boxes for the label and text entry should not overlap.
"field_label": "Last name",
"label_bounding_box": [30, 125, 95, 142],
"entry_bounding_box": [100, 125, 280, 142],
"entry_text": {
"text": "Johnson", // This text will be added as an annotation at the entry_bounding_box location
"font_size": 14, // optional, defaults to 14
"font_color": "000000", // optional, RRGGBB format, defaults to 000000 (black)
}
},
// Example for a checkbox. TARGET THE SQUARE for the entry bounding box, NOT THE TEXT
{
"page_number": 2,
"description": "Checkbox that should be checked if the user is over 18",
"entry_bounding_box": [140, 525, 155, 540], // Small box over checkbox square
"field_label": "Yes",
"label_bounding_box": [100, 525, 132, 540], // Box containing "Yes" text
// Use "X" to check a checkbox.
"entry_text": {
"text": "X",
}
}
// additional form field entries
]
}Create validation images by running this script from this file's directory for each page: `python scripts/create_validation_image.py <page_number> <path_to_fields.json> <input_image_path> <output_image_path>
The validation images will have red rectangles where text should be entered, and blue rectangles covering label text.
Step 3: Validate Bounding Boxes (REQUIRED)
Automated intersection check
- Verify that none of bounding boxes intersect and that the entry bounding boxes are tall enough by checking the fields.json file with the
check_bounding_boxes.pyscript (run from this file's directory):
python scripts/check_bounding_boxes.py <JSON file>
If there are errors, reanalyze the relevant fields, adjust the bounding boxes, and iterate until there are no remaining errors. Remember: label (blue) bounding boxes should contain text labels, entry (red) boxes should not.
Manual image inspection
CRITICAL: Do not proceed without visually inspecting validation images
- Red rectangles must ONLY cover input areas
- Red rectangles MUST NOT contain any text
- Blue rectangles should contain label text
- For checkboxes:
- Red rectangle MUST be centered on the checkbox square
- Blue rectangle should cover the text label for the checkbox
- If any rectangles look wrong, fix fields.json, regenerate the validation images, and verify again. Repeat this process until the bounding boxes are fully accurate.
Step 4: Add annotations to the PDF
Run this script from this file's directory to create a filled-out PDF using the information in fields.json: `python scripts/fill_pdf_form_with_annotations.py <input_pdf_path> <path_to_fields.json> <output_pdf_path>
© 2025 Anthropic, PBC. All rights reserved.
LICENSE: Use of these materials (including all code, prompts, assets, files,
and other components of this Skill) is governed by your agreement with
Anthropic regarding use of Anthropic's services. If no separate agreement
exists, use is governed by Anthropic's Consumer Terms of Service or
Commercial Terms of Service, as applicable:
https://www.anthropic.com/legal/consumer-terms
https://www.anthropic.com/legal/commercial-terms
Your applicable agreement is referred to as the "Agreement." "Services" are
as defined in the Agreement.
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
contrary, users may not:
- Extract these materials from the Services or retain copies of these
materials outside the Services
- Reproduce or copy these materials, except for temporary copies created
automatically during authorized use of the Services
- Create derivative works based on these materials
- Distribute, sublicense, or transfer these materials to any third party
- Make, offer to sell, sell, or import any inventions embodied in these
materials
- Reverse engineer, decompile, or disassemble these materials
The receipt, viewing, or possession of these materials does not convey or
imply any license or right beyond those expressly granted above.
Anthropic retains all right, title, and interest in these materials,
including all copyrights, patents, and other intellectual property rights.
PDF Processing Advanced Reference
This document contains advanced PDF processing features, detailed examples, and additional libraries not covered in the main skill instructions.
pypdfium2 Library (Apache/BSD License)
Overview
pypdfium2 is a Python binding for PDFium (Chromium's PDF library). It's excellent for fast PDF rendering, image generation, and serves as a PyMuPDF replacement.
Render PDF to Images
import pypdfium2 as pdfium
from PIL import Image
# Load PDF
pdf = pdfium.PdfDocument("document.pdf")
# Render page to image
page = pdf[0] # First page
bitmap = page.render(
scale=2.0, # Higher resolution
rotation=0 # No rotation
)
# Convert to PIL Image
img = bitmap.to_pil()
img.save("page_1.png", "PNG")
# Process multiple pages
for i, page in enumerate(pdf):
bitmap = page.render(scale=1.5)
img = bitmap.to_pil()
img.save(f"page_{i+1}.jpg", "JPEG", quality=90)Extract Text with pypdfium2
import pypdfium2 as pdfium
pdf = pdfium.PdfDocument("document.pdf")
for i, page in enumerate(pdf):
text = page.get_text()
print(f"Page {i+1} text length: {len(text)} chars")JavaScript Libraries
pdf-lib (MIT License)
pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment.
Load and Manipulate Existing PDF
import { PDFDocument } from "pdf-lib";
import fs from "fs";
async function manipulatePDF() {
// Load existing PDF
const existingPdfBytes = fs.readFileSync("input.pdf");
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Get page count
const pageCount = pdfDoc.getPageCount();
console.log(`Document has ${pageCount} pages`);
// Add new page
const newPage = pdfDoc.addPage([600, 400]);
newPage.drawText("Added by pdf-lib", {
x: 100,
y: 300,
size: 16,
});
// Save modified PDF
const pdfBytes = await pdfDoc.save();
fs.writeFileSync("modified.pdf", pdfBytes);
}Create Complex PDFs from Scratch
import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
import fs from "fs";
async function createPDF() {
const pdfDoc = await PDFDocument.create();
// Add fonts
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// Add page
const page = pdfDoc.addPage([595, 842]); // A4 size
const { width, height } = page.getSize();
// Add text with styling
page.drawText("Invoice #12345", {
x: 50,
y: height - 50,
size: 18,
font: helveticaBold,
color: rgb(0.2, 0.2, 0.8),
});
// Add rectangle (header background)
page.drawRectangle({
x: 40,
y: height - 100,
width: width - 80,
height: 30,
color: rgb(0.9, 0.9, 0.9),
});
// Add table-like content
const items = [
["Item", "Qty", "Price", "Total"],
["Widget", "2", "$50", "$100"],
["Gadget", "1", "$75", "$75"],
];
let yPos = height - 150;
items.forEach((row) => {
let xPos = 50;
row.forEach((cell) => {
page.drawText(cell, {
x: xPos,
y: yPos,
size: 12,
font: helveticaFont,
});
xPos += 120;
});
yPos -= 25;
});
const pdfBytes = await pdfDoc.save();
fs.writeFileSync("created.pdf", pdfBytes);
}Advanced Merge and Split Operations
import { PDFDocument } from "pdf-lib";
import fs from "fs";
async function mergePDFs() {
// Create new document
const mergedPdf = await PDFDocument.create();
// Load source PDFs
const pdf1Bytes = fs.readFileSync("doc1.pdf");
const pdf2Bytes = fs.readFileSync("doc2.pdf");
const pdf1 = await PDFDocument.load(pdf1Bytes);
const pdf2 = await PDFDocument.load(pdf2Bytes);
// Copy pages from first PDF
const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
pdf1Pages.forEach((page) => mergedPdf.addPage(page));
// Copy specific pages from second PDF (pages 0, 2, 4)
const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]);
pdf2Pages.forEach((page) => mergedPdf.addPage(page));
const mergedPdfBytes = await mergedPdf.save();
fs.writeFileSync("merged.pdf", mergedPdfBytes);
}pdfjs-dist (Apache License)
PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser.
Basic PDF Loading and Rendering
import * as pdfjsLib from "pdfjs-dist";
// Configure worker (important for performance)
pdfjsLib.GlobalWorkerOptions.workerSrc = "./pdf.worker.js";
async function renderPDF() {
// Load PDF
const loadingTask = pdfjsLib.getDocument("document.pdf");
const pdf = await loadingTask.promise;
console.log(`Loaded PDF with ${pdf.numPages} pages`);
// Get first page
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1.5 });
// Render to canvas
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
};
await page.render(renderContext).promise;
document.body.appendChild(canvas);
}Extract Text with Coordinates
import * as pdfjsLib from "pdfjs-dist";
async function extractText() {
const loadingTask = pdfjsLib.getDocument("document.pdf");
const pdf = await loadingTask.promise;
let fullText = "";
// Extract text from all pages
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items.map((item) => item.str).join(" ");
fullText += `\n--- Page ${i} ---\n${pageText}`;
// Get text with coordinates for advanced processing
const textWithCoords = textContent.items.map((item) => ({
text: item.str,
x: item.transform[4],
y: item.transform[5],
width: item.width,
height: item.height,
}));
}
console.log(fullText);
return fullText;
}Extract Annotations and Forms
import * as pdfjsLib from "pdfjs-dist";
async function extractAnnotations() {
const loadingTask = pdfjsLib.getDocument("annotated.pdf");
const pdf = await loadingTask.promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const annotations = await page.getAnnotations();
annotations.forEach((annotation) => {
console.log(`Annotation type: ${annotation.subtype}`);
console.log(`Content: ${annotation.contents}`);
console.log(`Coordinates: ${JSON.stringify(annotation.rect)}`);
});
}
}Advanced Command-Line Operations
poppler-utils Advanced Features
Extract Text with Bounding Box Coordinates
# Extract text with bounding box coordinates (essential for structured data)
pdftotext -bbox-layout document.pdf output.xml
# The XML output contains precise coordinates for each text elementAdvanced Image Conversion
# Convert to PNG images with specific resolution
pdftoppm -png -r 300 document.pdf output_prefix
# Convert specific page range with high resolution
pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages
# Convert to JPEG with quality setting
pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_outputExtract Embedded Images
# Extract all embedded images with metadata
pdfimages -j -p document.pdf page_images
# List image info without extracting
pdfimages -list document.pdf
# Extract images in their original format
pdfimages -all document.pdf images/imgqpdf Advanced Features
Complex Page Manipulation
# Split PDF into groups of pages
qpdf --split-pages=3 input.pdf output_group_%02d.pdf
# Extract specific pages with complex ranges
qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf
# Merge specific pages from multiple PDFs
qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdfPDF Optimization and Repair
# Optimize PDF for web (linearize for streaming)
qpdf --linearize input.pdf optimized.pdf
# Remove unused objects and compress
qpdf --optimize-level=all input.pdf compressed.pdf
# Attempt to repair corrupted PDF structure
qpdf --check input.pdf
qpdf --fix-qdf damaged.pdf repaired.pdf
# Show detailed PDF structure for debugging
qpdf --show-all-pages input.pdf > structure.txtAdvanced Encryption
# Add password protection with specific permissions
qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf
# Check encryption status
qpdf --show-encryption encrypted.pdf
# Remove password protection (requires password)
qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdfAdvanced Python Techniques
pdfplumber Advanced Features
Extract Text with Precise Coordinates
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
page = pdf.pages[0]
# Extract all text with coordinates
chars = page.chars
for char in chars[:10]: # First 10 characters
print(f"Char: '{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}")
# Extract text by bounding box (left, top, right, bottom)
bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text()Advanced Table Extraction with Custom Settings
import pdfplumber
import pandas as pd
with pdfplumber.open("complex_table.pdf") as pdf:
page = pdf.pages[0]
# Extract tables with custom settings for complex layouts
table_settings = {
"vertical_strategy": "lines",
"horizontal_strategy": "lines",
"snap_tolerance": 3,
"intersection_tolerance": 15
}
tables = page.extract_tables(table_settings)
# Visual debugging for table extraction
img = page.to_image(resolution=150)
img.save("debug_layout.png")reportlab Advanced Features
Create Professional Reports with Tables
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
# Sample data
data = [
['Product', 'Q1', 'Q2', 'Q3', 'Q4'],
['Widgets', '120', '135', '142', '158'],
['Gadgets', '85', '92', '98', '105']
]
# Create PDF with table
doc = SimpleDocTemplate("report.pdf")
elements = []
# Add title
styles = getSampleStyleSheet()
title = Paragraph("Quarterly Sales Report", styles['Title'])
elements.append(title)
# Add table with advanced styling
table = Table(data)
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 14),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black)
]))
elements.append(table)
doc.build(elements)Complex Workflows
Extract Figures/Images from PDF
Method 1: Using pdfimages (fastest)
# Extract all images with original quality
pdfimages -all document.pdf images/imgMethod 2: Using pypdfium2 + Image Processing
import pypdfium2 as pdfium
from PIL import Image
import numpy as np
def extract_figures(pdf_path, output_dir):
pdf = pdfium.PdfDocument(pdf_path)
for page_num, page in enumerate(pdf):
# Render high-resolution page
bitmap = page.render(scale=3.0)
img = bitmap.to_pil()
# Convert to numpy for processing
img_array = np.array(img)
# Simple figure detection (non-white regions)
mask = np.any(img_array != [255, 255, 255], axis=2)
# Find contours and extract bounding boxes
# (This is simplified - real implementation would need more sophisticated detection)
# Save detected figures
# ... implementation depends on specific needsBatch PDF Processing with Error Handling
import os
import glob
from pypdf import PdfReader, PdfWriter
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def batch_process_pdfs(input_dir, operation='merge'):
pdf_files = glob.glob(os.path.join(input_dir, "*.pdf"))
if operation == 'merge':
writer = PdfWriter()
for pdf_file in pdf_files:
try:
reader = PdfReader(pdf_file)
for page in reader.pages:
writer.add_page(page)
logger.info(f"Processed: {pdf_file}")
except Exception as e:
logger.error(f"Failed to process {pdf_file}: {e}")
continue
with open("batch_merged.pdf", "wb") as output:
writer.write(output)
elif operation == 'extract_text':
for pdf_file in pdf_files:
try:
reader = PdfReader(pdf_file)
text = ""
for page in reader.pages:
text += page.extract_text()
output_file = pdf_file.replace('.pdf', '.txt')
with open(output_file, 'w', encoding='utf-8') as f:
f.write(text)
logger.info(f"Extracted text from: {pdf_file}")
except Exception as e:
logger.error(f"Failed to extract text from {pdf_file}: {e}")
continueAdvanced PDF Cropping
from pypdf import PdfWriter, PdfReader
reader = PdfReader("input.pdf")
writer = PdfWriter()
# Crop page (left, bottom, right, top in points)
page = reader.pages[0]
page.mediabox.left = 50
page.mediabox.bottom = 50
page.mediabox.right = 550
page.mediabox.top = 750
writer.add_page(page)
with open("cropped.pdf", "wb") as output:
writer.write(output)Performance Optimization Tips
1. For Large PDFs
- Use streaming approaches instead of loading entire PDF in memory
- Use
qpdf --split-pagesfor splitting large files - Process pages individually with pypdfium2
2. For Text Extraction
pdftotext -bbox-layoutis fastest for plain text extraction- Use pdfplumber for structured data and tables
- Avoid
pypdf.extract_text()for very large documents
3. For Image Extraction
pdfimagesis much faster than rendering pages- Use low resolution for previews, high resolution for final output
4. For Form Filling
- pdf-lib maintains form structure better than most alternatives
- Pre-validate form fields before processing
5. Memory Management
# Process PDFs in chunks
def process_large_pdf(pdf_path, chunk_size=10):
reader = PdfReader(pdf_path)
total_pages = len(reader.pages)
for start_idx in range(0, total_pages, chunk_size):
end_idx = min(start_idx + chunk_size, total_pages)
writer = PdfWriter()
for i in range(start_idx, end_idx):
writer.add_page(reader.pages[i])
# Process chunk
with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output:
writer.write(output)Troubleshooting Common Issues
Encrypted PDFs
# Handle password-protected PDFs
from pypdf import PdfReader
try:
reader = PdfReader("encrypted.pdf")
if reader.is_encrypted:
reader.decrypt("password")
except Exception as e:
print(f"Failed to decrypt: {e}")Corrupted PDFs
# Use qpdf to repair
qpdf --check corrupted.pdf
qpdf --replace-input corrupted.pdfText Extraction Issues
# Fallback to OCR for scanned PDFs
import pytesseract
from pdf2image import convert_from_path
def extract_text_with_ocr(pdf_path):
images = convert_from_path(pdf_path)
text = ""
for i, image in enumerate(images):
text += pytesseract.image_to_string(image)
return textLicense Information
- pypdf: BSD License
- pdfplumber: MIT License
- pypdfium2: Apache/BSD License
- reportlab: BSD License
- poppler-utils: GPL-2 License
- qpdf: Apache License
- pdf-lib: MIT License
- pdfjs-dist: Apache License
HTML to PowerPoint Guide
Convert HTML slides to PowerPoint presentations with accurate positioning using the html2pptx.js library.
Table of Contents
1. Creating HTML Slides 2. Using the html2pptx Library 3. Using PptxGenJS
---
Creating HTML Slides
Every HTML slide must include proper body dimensions:
Layout Dimensions
- 16:9 (default):
width: 720pt; height: 405pt - 4:3:
width: 720pt; height: 540pt - 16:10:
width: 720pt; height: 450pt
Supported Elements
<p>,<h1>-<h6>- Text with styling<ul>,<ol>- Lists (never use manual bullets •, -, \*)<b>,<strong>- Bold text (inline formatting)<i>,<em>- Italic text (inline formatting)<u>- Underlined text (inline formatting)<span>- Inline formatting with CSS styles (bold, italic, underline, color)<br>- Line breaks<div>with bg/border - Becomes shape<img>- Imagesclass="placeholder"- Reserved space for charts (returns{ id, x, y, w, h })
Critical Text Rules
ALL text MUST be inside `<p>`, `<h1>`-`<h6>`, `<ul>`, or `<ol>` tags:
- ✅ Correct:
<div><p>Text here</p></div> - ❌ Wrong:
<div>Text here</div>- Text will NOT appear in PowerPoint - ❌ Wrong:
<span>Text</span>- Text will NOT appear in PowerPoint - Text in
<div>or<span>without a text tag will be silently ignored
*NEVER use manual bullet symbols (•, -, \, etc.)** - Use <ul> or <ol> lists instead
ONLY use web-safe fonts that are universally available:
- ✅ Web-safe fonts:
Arial,Helvetica,Times New Roman,Georgia,Courier New,Verdana,Tahoma,Trebuchet MS,Impact,Comic Sans MS - ❌ Wrong:
'Segoe UI','SF Pro','Roboto', custom fonts - Might cause rendering issues
Styling
- Use
display: flexon body to prevent margin collapse from breaking overflow validation - Use
marginfor spacing (padding included in size) - Inline formatting: Use
<b>,<i>,<u>tags OR<span>with CSS styles <span>supports:font-weight: bold,font-style: italic,text-decoration: underline,color: #rrggbb<span>does NOT support:margin,padding(not supported in PowerPoint text runs)- Example:
<span style="font-weight: bold; color: #667eea;">Bold blue text</span> - Flexbox works - positions calculated from rendered layout
- Use hex colors with
#prefix in CSS - Text alignment: Use CSS
text-align(center,right, etc.) when needed as a hint to PptxGenJS for text formatting if text lengths are slightly off
Shape Styling (DIV elements only)
IMPORTANT: Backgrounds, borders, and shadows only work on `<div>` elements, NOT on text elements (`<p>`, `<h1>`-`<h6>`, `<ul>`, `<ol>`)
- Backgrounds: CSS
backgroundorbackground-coloron<div>elements only - Example:
<div style="background: #f0f0f0;">- Creates a shape with background - Borders: CSS
borderon<div>elements converts to PowerPoint shape borders - Supports uniform borders:
border: 2px solid #333333 - Supports partial borders:
border-left,border-right,border-top,border-bottom(rendered as line shapes) - Example:
<div style="border-left: 8pt solid #E76F51;"> - Border radius: CSS
border-radiuson<div>elements for rounded corners border-radius: 50%or higher creates circular shape- Percentages <50% calculated relative to shape's smaller dimension
- Supports px and pt units (e.g.,
border-radius: 8pt;,border-radius: 12px;) - Example:
<div style="border-radius: 25%;">on 100x200px box = 25% of 100px = 25px radius - Box shadows: CSS
box-shadowon<div>elements converts to PowerPoint shadows - Supports outer shadows only (inset shadows are ignored to prevent corruption)
- Example:
<div style="box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);"> - Note: Inset/inner shadows are not supported by PowerPoint and will be skipped
Icons & Gradients
- CRITICAL: Never use CSS gradients (`linear-gradient`, `radial-gradient`) - They don't convert to PowerPoint
- ALWAYS create gradient/icon PNGs FIRST using Sharp, then reference in HTML
- For gradients: Rasterize SVG to PNG background images
- For icons: Rasterize react-icons SVG to PNG images
- All visual effects must be pre-rendered as raster images before HTML rendering
Rasterizing Icons with Sharp:
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const sharp = require("sharp");
const { FaHome } = require("react-icons/fa");
async function rasterizeIconPng(IconComponent, color, size = "256", filename) {
const svgString = ReactDOMServer.renderToStaticMarkup(
React.createElement(IconComponent, { color: `#${color}`, size: size }),
);
// Convert SVG to PNG using Sharp
await sharp(Buffer.from(svgString)).png().toFile(filename);
return filename;
}
// Usage: Rasterize icon before using in HTML
const iconPath = await rasterizeIconPng(
FaHome,
"4472c4",
"256",
"home-icon.png",
);
// Then reference in HTML: <img src="home-icon.png" style="width: 40pt; height: 40pt;">Rasterizing Gradients with Sharp:
const sharp = require("sharp");
async function createGradientBackground(filename) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="562.5">
<defs>
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#COLOR1"/>
<stop offset="100%" style="stop-color:#COLOR2"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#g)"/>
</svg>`;
await sharp(Buffer.from(svg)).png().toFile(filename);
return filename;
}
// Usage: Create gradient background before HTML
const bgPath = await createGradientBackground("gradient-bg.png");
// Then in HTML: <body style="background-image: url('gradient-bg.png');">Example
<!DOCTYPE html>
<html>
<head>
<style>
html {
background: #ffffff;
}
body {
width: 720pt;
height: 405pt;
margin: 0;
padding: 0;
background: #f5f5f5;
font-family: Arial, sans-serif;
display: flex;
}
.content {
margin: 30pt;
padding: 40pt;
background: #ffffff;
border-radius: 8pt;
}
h1 {
color: #2d3748;
font-size: 32pt;
}
.box {
background: #70ad47;
padding: 20pt;
border: 3px solid #5a8f37;
border-radius: 12pt;
box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.25);
}
</style>
</head>
<body>
<div class="content">
<h1>Recipe Title</h1>
<ul>
<li><b>Item:</b> Description</li>
</ul>
<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>.</p>
<div
id="chart"
class="placeholder"
style="width: 350pt; height: 200pt;"
></div>
<!-- Text MUST be in <p> tags -->
<div class="box">
<p>5</p>
</div>
</div>
</body>
</html>Using the html2pptx Library
Dependencies
These libraries have been globally installed and are available to use:
pptxgenjsplaywrightsharp
Basic Usage
const pptxgen = require("pptxgenjs");
const html2pptx = require("./html2pptx");
const pptx = new pptxgen();
pptx.layout = "LAYOUT_16x9"; // Must match HTML body dimensions
const { slide, placeholders } = await html2pptx("slide1.html", pptx);
// Add chart to placeholder area
if (placeholders.length > 0) {
slide.addChart(pptx.charts.LINE, chartData, placeholders[0]);
}
await pptx.writeFile("output.pptx");API Reference
Function Signature
await html2pptx(htmlFile, pres, options);Parameters
htmlFile(string): Path to HTML file (absolute or relative)pres(pptxgen): PptxGenJS presentation instance with layout already setoptions(object, optional):tmpDir(string): Temporary directory for generated files (default:process.env.TMPDIR || '/tmp')slide(object): Existing slide to reuse (default: creates new slide)
Returns
{
slide: pptxgenSlide, // The created/updated slide
placeholders: [ // Array of placeholder positions
{ id: string, x: number, y: number, w: number, h: number },
...
]
}Validation
The library automatically validates and collects all errors before throwing:
1. HTML dimensions must match presentation layout - Reports dimension mismatches 2. Content must not overflow body - Reports overflow with exact measurements 3. CSS gradients - Reports unsupported gradient usage 4. Text element styling - Reports backgrounds/borders/shadows on text elements (only allowed on divs)
All validation errors are collected and reported together in a single error message, allowing you to fix all issues at once instead of one at a time.
Working with Placeholders
const { slide, placeholders } = await html2pptx("slide.html", pptx);
// Use first placeholder
slide.addChart(pptx.charts.BAR, data, placeholders[0]);
// Find by ID
const chartArea = placeholders.find((p) => p.id === "chart-area");
slide.addChart(pptx.charts.LINE, data, chartArea);Complete Example
const pptxgen = require("pptxgenjs");
const html2pptx = require("./html2pptx");
async function createPresentation() {
const pptx = new pptxgen();
pptx.layout = "LAYOUT_16x9";
pptx.author = "Your Name";
pptx.title = "My Presentation";
// Slide 1: Title
const { slide: slide1 } = await html2pptx("slides/title.html", pptx);
// Slide 2: Content with chart
const { slide: slide2, placeholders } = await html2pptx(
"slides/data.html",
pptx,
);
const chartData = [
{
name: "Sales",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [4500, 5500, 6200, 7100],
},
];
slide2.addChart(pptx.charts.BAR, chartData, {
...placeholders[0],
showTitle: true,
title: "Quarterly Sales",
showCatAxisTitle: true,
catAxisTitle: "Quarter",
showValAxisTitle: true,
valAxisTitle: "Sales ($000s)",
});
// Save
await pptx.writeFile({ fileName: "presentation.pptx" });
console.log("Presentation created successfully!");
}
createPresentation().catch(console.error);Using PptxGenJS
After converting HTML to slides with html2pptx, you'll use PptxGenJS to add dynamic content like charts, images, and additional elements.
⚠️ Critical Rules
Colors
- NEVER use `#` prefix with hex colors in PptxGenJS - causes file corruption
- ✅ Correct:
color: "FF0000",fill: { color: "0066CC" } - ❌ Wrong:
color: "#FF0000"(breaks document)
Adding Images
Always calculate aspect ratios from actual image dimensions:
// Get image dimensions: identify image.png | grep -o '[0-9]* x [0-9]*'
const imgWidth = 1860,
imgHeight = 1519; // From actual file
const aspectRatio = imgWidth / imgHeight;
const h = 3; // Max height
const w = h * aspectRatio;
const x = (10 - w) / 2; // Center on 16:9 slide
slide.addImage({ path: "chart.png", x, y: 1.5, w, h });Adding Text
// Rich text with formatting
slide.addText(
[
{ text: "Bold ", options: { bold: true } },
{ text: "Italic ", options: { italic: true } },
{ text: "Normal" },
],
{
x: 1,
y: 2,
w: 8,
h: 1,
},
);Adding Shapes
// Rectangle
slide.addShape(pptx.shapes.RECTANGLE, {
x: 1,
y: 1,
w: 3,
h: 2,
fill: { color: "4472C4" },
line: { color: "000000", width: 2 },
});
// Circle
slide.addShape(pptx.shapes.OVAL, {
x: 5,
y: 1,
w: 2,
h: 2,
fill: { color: "ED7D31" },
});
// Rounded rectangle
slide.addShape(pptx.shapes.ROUNDED_RECTANGLE, {
x: 1,
y: 4,
w: 3,
h: 1.5,
fill: { color: "70AD47" },
rectRadius: 0.2,
});Adding Charts
Required for most charts: Axis labels using catAxisTitle (category) and valAxisTitle (value).
Chart Data Format:
- Use single series with all labels for simple bar/line charts
- Each series creates a separate legend entry
- Labels array defines X-axis values
Time Series Data - Choose Correct Granularity:
- < 30 days: Use daily grouping (e.g., "10-01", "10-02") - avoid monthly aggregation that creates single-point charts
- 30-365 days: Use monthly grouping (e.g., "2024-01", "2024-02")
- > 365 days: Use yearly grouping (e.g., "2023", "2024")
- Validate: Charts with only 1 data point likely indicate incorrect aggregation for the time period
const { slide, placeholders } = await html2pptx("slide.html", pptx);
// CORRECT: Single series with all labels
slide.addChart(
pptx.charts.BAR,
[
{
name: "Sales 2024",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [4500, 5500, 6200, 7100],
},
],
{
...placeholders[0], // Use placeholder position
barDir: "col", // 'col' = vertical bars, 'bar' = horizontal
showTitle: true,
title: "Quarterly Sales",
showLegend: false, // No legend needed for single series
// Required axis labels
showCatAxisTitle: true,
catAxisTitle: "Quarter",
showValAxisTitle: true,
valAxisTitle: "Sales ($000s)",
// Optional: Control scaling (adjust min based on data range for better visualization)
valAxisMaxVal: 8000,
valAxisMinVal: 0, // Use 0 for counts/amounts; for clustered data (e.g., 4500-7100), consider starting closer to min value
valAxisMajorUnit: 2000, // Control y-axis label spacing to prevent crowding
catAxisLabelRotate: 45, // Rotate labels if crowded
dataLabelPosition: "outEnd",
dataLabelColor: "000000",
// Use single color for single-series charts
chartColors: ["4472C4"], // All bars same color
},
);Scatter Chart
IMPORTANT: Scatter chart data format is unusual - first series contains X-axis values, subsequent series contain Y-values:
// Prepare data
const data1 = [
{ x: 10, y: 20 },
{ x: 15, y: 25 },
{ x: 20, y: 30 },
];
const data2 = [
{ x: 12, y: 18 },
{ x: 18, y: 22 },
];
const allXValues = [...data1.map((d) => d.x), ...data2.map((d) => d.x)];
slide.addChart(
pptx.charts.SCATTER,
[
{ name: "X-Axis", values: allXValues }, // First series = X values
{ name: "Series 1", values: data1.map((d) => d.y) }, // Y values only
{ name: "Series 2", values: data2.map((d) => d.y) }, // Y values only
],
{
x: 1,
y: 1,
w: 8,
h: 4,
lineSize: 0, // 0 = no connecting lines
lineDataSymbol: "circle",
lineDataSymbolSize: 6,
showCatAxisTitle: true,
catAxisTitle: "X Axis",
showValAxisTitle: true,
valAxisTitle: "Y Axis",
chartColors: ["4472C4", "ED7D31"],
},
);Line Chart
slide.addChart(
pptx.charts.LINE,
[
{
name: "Temperature",
labels: ["Jan", "Feb", "Mar", "Apr"],
values: [32, 35, 42, 55],
},
],
{
x: 1,
y: 1,
w: 8,
h: 4,
lineSize: 4,
lineSmooth: true,
// Required axis labels
showCatAxisTitle: true,
catAxisTitle: "Month",
showValAxisTitle: true,
valAxisTitle: "Temperature (°F)",
// Optional: Y-axis range (set min based on data range for better visualization)
valAxisMinVal: 0, // For ranges starting at 0 (counts, percentages, etc.)
valAxisMaxVal: 60,
valAxisMajorUnit: 20, // Control y-axis label spacing to prevent crowding (e.g., 10, 20, 25)
// valAxisMinVal: 30, // PREFERRED: For data clustered in a range (e.g., 32-55 or ratings 3-5), start axis closer to min value to show variation
// Optional: Chart colors
chartColors: ["4472C4", "ED7D31", "A5A5A5"],
},
);Pie Chart (No Axis Labels Required)
CRITICAL: Pie charts require a single data series with all categories in the labels array and corresponding values in the values array.
slide.addChart(
pptx.charts.PIE,
[
{
name: "Market Share",
labels: ["Product A", "Product B", "Other"], // All categories in one array
values: [35, 45, 20], // All values in one array
},
],
{
x: 2,
y: 1,
w: 6,
h: 4,
showPercent: true,
showLegend: true,
legendPos: "r", // right
chartColors: ["4472C4", "ED7D31", "A5A5A5"],
},
);Multiple Data Series
slide.addChart(
pptx.charts.LINE,
[
{
name: "Product A",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [10, 20, 30, 40],
},
{
name: "Product B",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [15, 25, 20, 35],
},
],
{
x: 1,
y: 1,
w: 8,
h: 4,
showCatAxisTitle: true,
catAxisTitle: "Quarter",
showValAxisTitle: true,
valAxisTitle: "Revenue ($M)",
},
);Chart Colors
CRITICAL: Use hex colors without the # prefix - including # causes file corruption.
Align chart colors with your chosen design palette, ensuring sufficient contrast and distinctiveness for data visualization. Adjust colors for:
- Strong contrast between adjacent series
- Readability against slide backgrounds
- Accessibility (avoid red-green only combinations)
// Example: Ocean palette-inspired chart colors (adjusted for contrast)
const chartColors = ["16A085", "FF6B9D", "2C3E50", "F39C12", "9B59B6"];
// Single-series chart: Use one color for all bars/points
slide.addChart(
pptx.charts.BAR,
[
{
name: "Sales",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [4500, 5500, 6200, 7100],
},
],
{
...placeholders[0],
chartColors: ["16A085"], // All bars same color
showLegend: false,
},
);
// Multi-series chart: Each series gets a different color
slide.addChart(
pptx.charts.LINE,
[
{ name: "Product A", labels: ["Q1", "Q2", "Q3"], values: [10, 20, 30] },
{ name: "Product B", labels: ["Q1", "Q2", "Q3"], values: [15, 25, 20] },
],
{
...placeholders[0],
chartColors: ["16A085", "FF6B9D"], // One color per series
},
);Adding Tables
Tables can be added with basic or advanced formatting:
Basic Table
slide.addTable(
[
["Header 1", "Header 2", "Header 3"],
["Row 1, Col 1", "Row 1, Col 2", "Row 1, Col 3"],
["Row 2, Col 1", "Row 2, Col 2", "Row 2, Col 3"],
],
{
x: 0.5,
y: 1,
w: 9,
h: 3,
border: { pt: 1, color: "999999" },
fill: { color: "F1F1F1" },
},
);Table with Custom Formatting
const tableData = [
// Header row with custom styling
[
{
text: "Product",
options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true },
},
{
text: "Revenue",
options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true },
},
{
text: "Growth",
options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true },
},
],
// Data rows
["Product A", "$50M", "+15%"],
["Product B", "$35M", "+22%"],
["Product C", "$28M", "+8%"],
];
slide.addTable(tableData, {
x: 1,
y: 1.5,
w: 8,
h: 3,
colW: [3, 2.5, 2.5], // Column widths
rowH: [0.5, 0.6, 0.6, 0.6], // Row heights
border: { pt: 1, color: "CCCCCC" },
align: "center",
valign: "middle",
fontSize: 14,
});Table with Merged Cells
const mergedTableData = [
[
{
text: "Q1 Results",
options: {
colspan: 3,
fill: { color: "4472C4" },
color: "FFFFFF",
bold: true,
},
},
],
["Product", "Sales", "Market Share"],
["Product A", "$25M", "35%"],
["Product B", "$18M", "25%"],
];
slide.addTable(mergedTableData, {
x: 1,
y: 1,
w: 8,
h: 2.5,
colW: [3, 2.5, 2.5],
border: { pt: 1, color: "DDDDDD" },
});Table Options
Common table options:
x, y, w, h- Position and sizecolW- Array of column widths (in inches)rowH- Array of row heights (in inches)border- Border style:{ pt: 1, color: "999999" }fill- Background color (no # prefix)align- Text alignment: "left", "center", "right"valign- Vertical alignment: "top", "middle", "bottom"fontSize- Text sizeautoPage- Auto-create new slides if content overflows
© 2025 Anthropic, PBC. All rights reserved.
LICENSE: Use of these materials (including all code, prompts, assets, files,
and other components of this Skill) is governed by your agreement with
Anthropic regarding use of Anthropic's services. If no separate agreement
exists, use is governed by Anthropic's Consumer Terms of Service or
Commercial Terms of Service, as applicable:
https://www.anthropic.com/legal/consumer-terms
https://www.anthropic.com/legal/commercial-terms
Your applicable agreement is referred to as the "Agreement." "Services" are
as defined in the Agreement.
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
contrary, users may not:
- Extract these materials from the Services or retain copies of these
materials outside the Services
- Reproduce or copy these materials, except for temporary copies created
automatically during authorized use of the Services
- Create derivative works based on these materials
- Distribute, sublicense, or transfer these materials to any third party
- Make, offer to sell, sell, or import any inventions embodied in these
materials
- Reverse engineer, decompile, or disassemble these materials
The receipt, viewing, or possession of these materials does not convey or
imply any license or right beyond those expressly granted above.
Anthropic retains all right, title, and interest in these materials,
including all copyrights, patents, and other intellectual property rights.
Office Open XML Technical Reference for PowerPoint
Important: Read this entire document before starting. Critical XML schema rules and formatting requirements are covered throughout. Incorrect implementation can create invalid PPTX files that PowerPoint cannot open.
Technical Guidelines
Schema Compliance
- Element ordering in `<p:txBody>`:
<a:bodyPr>,<a:lstStyle>,<a:p> - Whitespace: Add
xml:space='preserve'to<a:t>elements with leading/trailing spaces - Unicode: Escape characters in ASCII content:
"becomes“ - Images: Add to
ppt/media/, reference in slide XML, set dimensions to fit slide bounds - Relationships: Update
ppt/slides/_rels/slideN.xml.relsfor each slide's resources - Dirty attribute: Add
dirty="0"to<a:rPr>and<a:endParaRPr>elements to indicate clean state
Presentation Structure
Basic Slide Structure
<!-- ppt/slides/slide1.xml -->
<p:sld>
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>...</p:nvGrpSpPr>
<p:grpSpPr>...</p:grpSpPr>
<!-- Shapes go here -->
</p:spTree>
</p:cSld>
</p:sld>Text Box / Shape with Text
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Title"/>
<p:cNvSpPr>
<a:spLocks noGrp="1"/>
</p:cNvSpPr>
<p:nvPr>
<p:ph type="ctrTitle"/>
</p:nvPr>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="838200" y="365125"/>
<a:ext cx="7772400" cy="1470025"/>
</a:xfrm>
</p:spPr>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:t>Slide Title</a:t>
</a:r>
</a:p>
</p:txBody>
</p:sp>Text Formatting
<!-- Bold -->
<a:r>
<a:rPr b="1"/>
<a:t>Bold Text</a:t>
</a:r>
<!-- Italic -->
<a:r>
<a:rPr i="1"/>
<a:t>Italic Text</a:t>
</a:r>
<!-- Underline -->
<a:r>
<a:rPr u="sng"/>
<a:t>Underlined</a:t>
</a:r>
<!-- Highlight -->
<a:r>
<a:rPr>
<a:highlight>
<a:srgbClr val="FFFF00"/>
</a:highlight>
</a:rPr>
<a:t>Highlighted Text</a:t>
</a:r>
<!-- Font and Size -->
<a:r>
<a:rPr sz="2400" typeface="Arial">
<a:solidFill>
<a:srgbClr val="FF0000"/>
</a:solidFill>
</a:rPr>
<a:t>Colored Arial 24pt</a:t>
</a:r>
<!-- Complete formatting example -->
<a:r>
<a:rPr lang="en-US" sz="1400" b="1" dirty="0">
<a:solidFill>
<a:srgbClr val="FAFAFA"/>
</a:solidFill>
</a:rPr>
<a:t>Formatted text</a:t>
</a:r>Lists
<!-- Bullet list -->
<a:p>
<a:pPr lvl="0">
<a:buChar char="•"/>
</a:pPr>
<a:r>
<a:t>First bullet point</a:t>
</a:r>
</a:p>
<!-- Numbered list -->
<a:p>
<a:pPr lvl="0">
<a:buAutoNum type="arabicPeriod"/>
</a:pPr>
<a:r>
<a:t>First numbered item</a:t>
</a:r>
</a:p>
<!-- Second level indent -->
<a:p>
<a:pPr lvl="1">
<a:buChar char="•"/>
</a:pPr>
<a:r>
<a:t>Indented bullet</a:t>
</a:r>
</a:p>Shapes
<!-- Rectangle -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="3" name="Rectangle"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="1000000" y="1000000"/>
<a:ext cx="3000000" cy="2000000"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
<a:solidFill>
<a:srgbClr val="FF0000"/>
</a:solidFill>
<a:ln w="25400">
<a:solidFill>
<a:srgbClr val="000000"/>
</a:solidFill>
</a:ln>
</p:spPr>
</p:sp>
<!-- Rounded Rectangle -->
<p:sp>
<p:spPr>
<a:prstGeom prst="roundRect">
<a:avLst/>
</a:prstGeom>
</p:spPr>
</p:sp>
<!-- Circle/Ellipse -->
<p:sp>
<p:spPr>
<a:prstGeom prst="ellipse">
<a:avLst/>
</a:prstGeom>
</p:spPr>
</p:sp>Images
<p:pic>
<p:nvPicPr>
<p:cNvPr id="4" name="Picture">
<a:hlinkClick r:id="" action="ppaction://media"/>
</p:cNvPr>
<p:cNvPicPr>
<a:picLocks noChangeAspect="1"/>
</p:cNvPicPr>
<p:nvPr/>
</p:nvPicPr>
<p:blipFill>
<a:blip r:embed="rId2"/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</p:blipFill>
<p:spPr>
<a:xfrm>
<a:off x="1000000" y="1000000"/>
<a:ext cx="3000000" cy="2000000"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</p:spPr>
</p:pic>Tables
<p:graphicFrame>
<p:nvGraphicFramePr>
<p:cNvPr id="5" name="Table"/>
<p:cNvGraphicFramePr>
<a:graphicFrameLocks noGrp="1"/>
</p:cNvGraphicFramePr>
<p:nvPr/>
</p:nvGraphicFramePr>
<p:xfrm>
<a:off x="1000000" y="1000000"/>
<a:ext cx="6000000" cy="2000000"/>
</p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
<a:tbl>
<a:tblGrid>
<a:gridCol w="3000000"/>
<a:gridCol w="3000000"/>
</a:tblGrid>
<a:tr h="500000">
<a:tc>
<a:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:t>Cell 1</a:t>
</a:r>
</a:p>
</a:txBody>
</a:tc>
<a:tc>
<a:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:t>Cell 2</a:t>
</a:r>
</a:p>
</a:txBody>
</a:tc>
</a:tr>
</a:tbl>
</a:graphicData>
</a:graphic>
</p:graphicFrame>Slide Layouts
<!-- Title Slide Layout -->
<p:sp>
<p:nvSpPr>
<p:nvPr>
<p:ph type="ctrTitle"/>
</p:nvPr>
</p:nvSpPr>
<!-- Title content -->
</p:sp>
<p:sp>
<p:nvSpPr>
<p:nvPr>
<p:ph type="subTitle" idx="1"/>
</p:nvPr>
</p:nvSpPr>
<!-- Subtitle content -->
</p:sp>
<!-- Content Slide Layout -->
<p:sp>
<p:nvSpPr>
<p:nvPr>
<p:ph type="title"/>
</p:nvPr>
</p:nvSpPr>
<!-- Slide title -->
</p:sp>
<p:sp>
<p:nvSpPr>
<p:nvPr>
<p:ph type="body" idx="1"/>
</p:nvPr>
</p:nvSpPr>
<!-- Content body -->
</p:sp>File Updates
When adding content, update these files:
`ppt/_rels/presentation.xml.rels`:
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>`ppt/slides/_rels/slide1.xml.rels`:
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/>`[Content_Types].xml`:
<Default Extension="png" ContentType="image/png"/>
<Default Extension="jpg" ContentType="image/jpeg"/>
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>`ppt/presentation.xml`:
<p:sldIdLst>
<p:sldId id="256" r:id="rId1"/>
<p:sldId id="257" r:id="rId2"/>
</p:sldIdLst>`docProps/app.xml`: Update slide count and statistics
<Slides>2</Slides>
<Paragraphs>10</Paragraphs>
<Words>50</Words>Slide Operations
Adding a New Slide
When adding a slide to the end of the presentation:
1. Create the slide file (ppt/slides/slideN.xml) 2. Update `[Content_Types].xml`: Add Override for the new slide 3. Update `ppt/_rels/presentation.xml.rels`: Add relationship for the new slide 4. Update `ppt/presentation.xml`: Add slide ID to <p:sldIdLst> 5. Create slide relationships (ppt/slides/_rels/slideN.xml.rels) if needed 6. Update `docProps/app.xml`: Increment slide count and update statistics (if present)
Duplicating a Slide
1. Copy the source slide XML file with a new name 2. Update all IDs in the new slide to be unique 3. Follow the "Adding a New Slide" steps above 4. CRITICAL: Remove or update any notes slide references in _rels files 5. Remove references to unused media files
Reordering Slides
1. Update `ppt/presentation.xml`: Reorder <p:sldId> elements in <p:sldIdLst> 2. The order of <p:sldId> elements determines slide order 3. Keep slide IDs and relationship IDs unchanged
Example:
<!-- Original order -->
<p:sldIdLst>
<p:sldId id="256" r:id="rId2"/>
<p:sldId id="257" r:id="rId3"/>
<p:sldId id="258" r:id="rId4"/>
</p:sldIdLst>
<!-- After moving slide 3 to position 2 -->
<p:sldIdLst>
<p:sldId id="256" r:id="rId2"/>
<p:sldId id="258" r:id="rId4"/>
<p:sldId id="257" r:id="rId3"/>
</p:sldIdLst>Deleting a Slide
1. Remove from `ppt/presentation.xml`: Delete the <p:sldId> entry 2. Remove from `ppt/_rels/presentation.xml.rels`: Delete the relationship 3. Remove from `[Content_Types].xml`: Delete the Override entry 4. Delete files: Remove ppt/slides/slideN.xml and ppt/slides/_rels/slideN.xml.rels 5. Update `docProps/app.xml`: Decrement slide count and update statistics 6. Clean up unused media: Remove orphaned images from ppt/media/
Note: Don't renumber remaining slides - keep their original IDs and filenames.
Common Errors to Avoid
- Encodings: Escape unicode characters in ASCII content:
"becomes“ - Images: Add to
ppt/media/and update relationship files - Lists: Omit bullets from list headers
- IDs: Use valid hexadecimal values for UUIDs
- Themes: Check all themes in
themedirectory for colors
Validation Checklist for Template-Based Presentations
Before Packing, Always:
- Clean unused resources: Remove unreferenced media, fonts, and notes directories
- Fix Content_Types.xml: Declare ALL slides, layouts, and themes present in the package
- Fix relationship IDs:
- Remove font embed references if not using embedded fonts
- Remove broken references: Check all
_relsfiles for references to deleted resources
Common Template Duplication Pitfalls:
- Multiple slides referencing the same notes slide after duplication
- Image/media references from template slides that no longer exist
- Font embedding references when fonts aren't included
- Missing slideLayout declarations for layouts 12-25
- docProps directory may not unpack - this is optional
© 2025 Anthropic, PBC. All rights reserved.
LICENSE: Use of these materials (including all code, prompts, assets, files,
and other components of this Skill) is governed by your agreement with
Anthropic regarding use of Anthropic's services. If no separate agreement
exists, use is governed by Anthropic's Consumer Terms of Service or
Commercial Terms of Service, as applicable:
https://www.anthropic.com/legal/consumer-terms
https://www.anthropic.com/legal/commercial-terms
Your applicable agreement is referred to as the "Agreement." "Services" are
as defined in the Agreement.
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
contrary, users may not:
- Extract these materials from the Services or retain copies of these
materials outside the Services
- Reproduce or copy these materials, except for temporary copies created
automatically during authorized use of the Services
- Create derivative works based on these materials
- Distribute, sublicense, or transfer these materials to any third party
- Make, offer to sell, sell, or import any inventions embodied in these
materials
- Reverse engineer, decompile, or disassemble these materials
The receipt, viewing, or possession of these materials does not convey or
imply any license or right beyond those expressly granted above.
Anthropic retains all right, title, and interest in these materials,
including all copyrights, patents, and other intellectual property rights.
#!/usr/bin/env python3
"""
Excel Formula Recalculation Script
Recalculates all formulas in an Excel file using LibreOffice
"""
import json
import sys
import subprocess
import os
import platform
from pathlib import Path
from openpyxl import load_workbook
def setup_libreoffice_macro():
"""Setup LibreOffice macro for recalculation if not already configured"""
if platform.system() == 'Darwin':
macro_dir = os.path.expanduser('~/Library/Application Support/LibreOffice/4/user/basic/Standard')
else:
macro_dir = os.path.expanduser('~/.config/libreoffice/4/user/basic/Standard')
macro_file = os.path.join(macro_dir, 'Module1.xba')
if os.path.exists(macro_file):
with open(macro_file, 'r') as f:
if 'RecalculateAndSave' in f.read():
return True
if not os.path.exists(macro_dir):
subprocess.run(['soffice', '--headless', '--terminate_after_init'],
capture_output=True, timeout=10)
os.makedirs(macro_dir, exist_ok=True)
macro_content = '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
Sub RecalculateAndSave()
ThisComponent.calculateAll()
ThisComponent.store()
ThisComponent.close(True)
End Sub
</script:module>'''
try:
with open(macro_file, 'w') as f:
f.write(macro_content)
return True
except Exception:
return False
def recalc(filename, timeout=30):
"""
Recalculate formulas in Excel file and report any errors
Args:
filename: Path to Excel file
timeout: Maximum time to wait for recalculation (seconds)
Returns:
dict with error locations and counts
"""
if not Path(filename).exists():
return {'error': f'File {filename} does not exist'}
abs_path = str(Path(filename).absolute())
if not setup_libreoffice_macro():
return {'error': 'Failed to setup LibreOffice macro'}
cmd = [
'soffice', '--headless', '--norestore',
'vnd.sun.star.script:Standard.Module1.RecalculateAndSave?language=Basic&location=application',
abs_path
]
# Handle timeout command differences between Linux and macOS
if platform.system() != 'Windows':
timeout_cmd = 'timeout' if platform.system() == 'Linux' else None
if platform.system() == 'Darwin':
# Check if gtimeout is available on macOS
try:
subprocess.run(['gtimeout', '--version'], capture_output=True, timeout=1, check=False)
timeout_cmd = 'gtimeout'
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
if timeout_cmd:
cmd = [timeout_cmd, str(timeout)] + cmd
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0 and result.returncode != 124: # 124 is timeout exit code
error_msg = result.stderr or 'Unknown error during recalculation'
if 'Module1' in error_msg or 'RecalculateAndSave' not in error_msg:
return {'error': 'LibreOffice macro not configured properly'}
else:
return {'error': error_msg}
# Check for Excel errors in the recalculated file - scan ALL cells
try:
wb = load_workbook(filename, data_only=True)
excel_errors = ['#VALUE!', '#DIV/0!', '#REF!', '#NAME?', '#NULL!', '#NUM!', '#N/A']
error_details = {err: [] for err in excel_errors}
total_errors = 0
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
# Check ALL rows and columns - no limits
for row in ws.iter_rows():
for cell in row:
if cell.value is not None and isinstance(cell.value, str):
for err in excel_errors:
if err in cell.value:
location = f"{sheet_name}!{cell.coordinate}"
error_details[err].append(location)
total_errors += 1
break
wb.close()
# Build result summary
result = {
'status': 'success' if total_errors == 0 else 'errors_found',
'total_errors': total_errors,
'error_summary': {}
}
# Add non-empty error categories
for err_type, locations in error_details.items():
if locations:
result['error_summary'][err_type] = {
'count': len(locations),
'locations': locations[:20] # Show up to 20 locations
}
# Add formula count for context - also check ALL cells
wb_formulas = load_workbook(filename, data_only=False)
formula_count = 0
for sheet_name in wb_formulas.sheetnames:
ws = wb_formulas[sheet_name]
for row in ws.iter_rows():
for cell in row:
if cell.value and isinstance(cell.value, str) and cell.value.startswith('='):
formula_count += 1
wb_formulas.close()
result['total_formulas'] = formula_count
return result
except Exception as e:
return {'error': str(e)}
def main():
if len(sys.argv) < 2:
print("Usage: python recalc.py <excel_file> [timeout_seconds]")
print("\nRecalculates all formulas in an Excel file using LibreOffice")
print("\nReturns JSON with error details:")
print(" - status: 'success' or 'errors_found'")
print(" - total_errors: Total number of Excel errors found")
print(" - total_formulas: Number of formulas in the file")
print(" - error_summary: Breakdown by error type with locations")
print(" - #VALUE!, #DIV/0!, #REF!, #NAME?, #NULL!, #NUM!, #N/A")
sys.exit(1)
filename = sys.argv[1]
timeout = int(sys.argv[2]) if len(sys.argv) > 2 else 30
result = recalc(filename, timeout)
print(json.dumps(result, indent=2))
if __name__ == '__main__':
main()Related skills
How it compares
Multi-format Office document suite with sub-skill routing, not a single-format snippet.
FAQ
Which sub-skills does document-skills include?
docx for Word, pdf for PDF extraction and merge or split, pptx for presentations, and xlsx for spreadsheets.
How should agents load a specific format?
Identify the document type, then invoke the matching sub-skill such as Skill(document-skills/docx) and follow its SKILL.md workflow.
Is Document Skills safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.