` elements converts to PowerPoint shadows
+ - Supports outer shadows only (inset shadows are ignored to prevent corruption)
+ - Example: `
`
+ - 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:**
+
+```javascript
+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:

+```
+
+**Rasterizing Gradients with Sharp:**
+
+```javascript
+const sharp = require(''sharp'');
+
+async function createGradientBackground(filename) {
+ const 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:
+```
+
+### Example
+
+```html
+
+
+
+
+
+
+
+
Recipe Title
+
+
Text with bold, italic, underline.
+
+
+
+
+
+
+
+```
+
+## Using the html2pptx Library
+
+### Dependencies
+
+These libraries have been globally installed and are available to use:
+- `pptxgenjs`
+- `playwright`
+- `sharp`
+
+### Basic Usage
+
+```javascript
+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
+```javascript
+await html2pptx(htmlFile, pres, options)
+```
+
+#### Parameters
+- `htmlFile` (string): Path to HTML file (absolute or relative)
+- `pres` (pptxgen): PptxGenJS presentation instance with layout already set
+- `options` (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
+```javascript
+{
+ 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
+
+```javascript
+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
+
+```javascript
+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:
+
+```javascript
+// 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
+
+```javascript
+// 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
+
+```javascript
+// 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
+
+```javascript
+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:
+
+```javascript
+// 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
+
+```javascript
+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.
+
+```javascript
+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
+
+```javascript
+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)
+
+```javascript
+// 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
+
+```javascript
+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
+
+```javascript
+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
+
+```javascript
+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 size
+- `colW` - 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 size
+- `autoPage` - Auto-create new slides if content overflows' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml.md', '# 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 `
`**: ``, ``, ``
+- **Whitespace**: Add `xml:space=''preserve''` to `` 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.rels` for each slide''s resources
+- **Dirty attribute**: Add `dirty="0"` to `` and `` elements to indicate clean state
+
+## Presentation Structure
+
+### Basic Slide Structure
+```xml
+
+
+
+
+ ...
+ ...
+
+
+
+
+```
+
+### Text Box / Shape with Text
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Slide Title
+
+
+
+
+```
+
+### Text Formatting
+```xml
+
+
+
+ Bold Text
+
+
+
+
+
+ Italic Text
+
+
+
+
+
+ Underlined
+
+
+
+
+
+
+
+
+
+ Highlighted Text
+
+
+
+
+
+
+
+
+
+ Colored Arial 24pt
+
+
+
+
+
+
+
+
+
+ Formatted text
+
+```
+
+### Lists
+```xml
+
+
+
+
+
+
+ First bullet point
+
+
+
+
+
+
+
+
+
+ First numbered item
+
+
+
+
+
+
+
+
+
+ Indented bullet
+
+
+```
+
+### Shapes
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Images
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Tables
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cell 1
+
+
+
+
+
+
+
+
+
+
+ Cell 2
+
+
+
+
+
+
+
+
+
+```
+
+### Slide Layouts
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## File Updates
+
+When adding content, update these files:
+
+**`ppt/_rels/presentation.xml.rels`:**
+```xml
+
+
+```
+
+**`ppt/slides/_rels/slide1.xml.rels`:**
+```xml
+
+
+```
+
+**`[Content_Types].xml`:**
+```xml
+
+
+
+```
+
+**`ppt/presentation.xml`:**
+```xml
+
+
+
+
+```
+
+**`docProps/app.xml`:** Update slide count and statistics
+```xml
+2
+10
+50
+```
+
+## 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 ``
+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 `` elements in ``
+2. The order of `` elements determines slide order
+3. Keep slide IDs and relationship IDs unchanged
+
+Example:
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Deleting a Slide
+1. **Remove from `ppt/presentation.xml`**: Delete the `` 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 `theme` directory 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 `_rels` files 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' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd', '
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd', E'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd', E'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd', '
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd', '
+
+
+
+
+ See http://www.w3.org/XML/1998/namespace.html and
+ http://www.w3.org/TR/REC-xml for information about this namespace.
+
+ This schema document describes the XML namespace, in a form
+ suitable for import by other schema documents.
+
+ Note that local names in this namespace are intended to be defined
+ only by the World Wide Web Consortium or its subgroups. The
+ following names are currently defined in this namespace and should
+ not be used with conflicting semantics by any Working Group,
+ specification, or document instance:
+
+ base (as an attribute name): denotes an attribute whose value
+ provides a URI to be used as the base for interpreting any
+ relative URIs in the scope of the element on which it
+ appears; its value is inherited. This name is reserved
+ by virtue of its definition in the XML Base specification.
+
+ lang (as an attribute name): denotes an attribute whose value
+ is a language code for the natural language of the content of
+ any element; its value is inherited. This name is reserved
+ by virtue of its definition in the XML specification.
+
+ space (as an attribute name): denotes an attribute whose
+ value is a keyword indicating what whitespace processing
+ discipline is intended for the content of the element; its
+ value is inherited. This name is reserved by virtue of its
+ definition in the XML specification.
+
+ Father (in any context at all): denotes Jon Bosak, the chair of
+ the original XML Working Group. This name is reserved by
+ the following decision of the W3C XML Plenary and
+ XML Coordination groups:
+
+ In appreciation for his vision, leadership and dedication
+ the W3C XML Plenary on this 10th day of February, 2000
+ reserves for Jon Bosak in perpetuity the XML name
+ xml:Father
+
+
+
+
+ This schema defines attributes and an attribute group
+ suitable for use by
+ schemas wishing to allow xml:base, xml:lang or xml:space attributes
+ on elements they define.
+
+ To enable this, such a schema must import this schema
+ for the XML namespace, e.g. as follows:
+ <schema . . .>
+ . . .
+ <import namespace="http://www.w3.org/XML/1998/namespace"
+ schemaLocation="http://www.w3.org/2001/03/xml.xsd"/>
+
+ Subsequently, qualified reference to any of the attributes
+ or the group defined below will have the desired effect, e.g.
+
+ <type . . .>
+ . . .
+ <attributeGroup ref="xml:specialAttrs"/>
+
+ will define a type which will schema-validate an instance
+ element with any of those attributes
+
+
+
+ In keeping with the XML Schema WG''s standard versioning
+ policy, this schema document will persist at
+ http://www.w3.org/2001/03/xml.xsd.
+ At the date of issue it can also be found at
+ http://www.w3.org/2001/xml.xsd.
+ The schema document at that URI may however change in the future,
+ in order to remain compatible with the latest version of XML Schema
+ itself. In other words, if the XML Schema namespace changes, the version
+ of this document at
+ http://www.w3.org/2001/xml.xsd will change
+ accordingly; the version at
+ http://www.w3.org/2001/03/xml.xsd will not change.
+
+
+
+
+
+ In due course, we should install the relevant ISO 2- and 3-letter
+ codes as the enumerated possible values . . .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ See http://www.w3.org/TR/xmlbase/ for
+ information about this attribute.
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd', E'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd', E'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/mce/mc.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/microsoft/wml-2010.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/microsoft/wml-2012.xsd', E'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/microsoft/wml-2018.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/microsoft/wml-cex-2018.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/microsoft/wml-cid-2016.xsd', '
+
+
+
+
+
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd', '
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/schemas/microsoft/wml-symex-2015.xsd', '
+
+
+
+
+
+
+
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/scripts/pack.py', '#!/usr/bin/env python3
+"""
+Tool to pack a directory into a .docx, .pptx, or .xlsx file with XML formatting undone.
+
+Example usage:
+ python pack.py [--force]
+"""
+
+import argparse
+import shutil
+import subprocess
+import sys
+import tempfile
+import defusedxml.minidom
+import zipfile
+from pathlib import Path
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Pack a directory into an Office file")
+ parser.add_argument("input_directory", help="Unpacked Office document directory")
+ parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)")
+ parser.add_argument("--force", action="store_true", help="Skip validation")
+ args = parser.parse_args()
+
+ try:
+ success = pack_document(
+ args.input_directory, args.output_file, validate=not args.force
+ )
+
+ # Show warning if validation was skipped
+ if args.force:
+ print("Warning: Skipped validation, file may be corrupt", file=sys.stderr)
+ # Exit with error if validation failed
+ elif not success:
+ print("Contents would produce a corrupt file.", file=sys.stderr)
+ print("Please validate XML before repacking.", file=sys.stderr)
+ print("Use --force to skip validation and pack anyway.", file=sys.stderr)
+ sys.exit(1)
+
+ except ValueError as e:
+ sys.exit(f"Error: {e}")
+
+
+def pack_document(input_dir, output_file, validate=False):
+ """Pack a directory into an Office file (.docx/.pptx/.xlsx).
+
+ Args:
+ input_dir: Path to unpacked Office document directory
+ output_file: Path to output Office file
+ validate: If True, validates with soffice (default: False)
+
+ Returns:
+ bool: True if successful, False if validation failed
+ """
+ input_dir = Path(input_dir)
+ output_file = Path(output_file)
+
+ if not input_dir.is_dir():
+ raise ValueError(f"{input_dir} is not a directory")
+ if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}:
+ raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file")
+
+ # Work in temporary directory to avoid modifying original
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_content_dir = Path(temp_dir) / "content"
+ shutil.copytree(input_dir, temp_content_dir)
+
+ # Process XML files to remove pretty-printing whitespace
+ for pattern in ["*.xml", "*.rels"]:
+ for xml_file in temp_content_dir.rglob(pattern):
+ condense_xml(xml_file)
+
+ # Create final Office file as zip archive
+ output_file.parent.mkdir(parents=True, exist_ok=True)
+ with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf:
+ for f in temp_content_dir.rglob("*"):
+ if f.is_file():
+ zf.write(f, f.relative_to(temp_content_dir))
+
+ # Validate if requested
+ if validate:
+ if not validate_document(output_file):
+ output_file.unlink() # Delete the corrupt file
+ return False
+
+ return True
+
+
+def validate_document(doc_path):
+ """Validate document by converting to HTML with soffice."""
+ # Determine the correct filter based on file extension
+ match doc_path.suffix.lower():
+ case ".docx":
+ filter_name = "html:HTML"
+ case ".pptx":
+ filter_name = "html:impress_html_Export"
+ case ".xlsx":
+ filter_name = "html:HTML (StarCalc)"
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ try:
+ result = subprocess.run(
+ [
+ "soffice",
+ "--headless",
+ "--convert-to",
+ filter_name,
+ "--outdir",
+ temp_dir,
+ str(doc_path),
+ ],
+ capture_output=True,
+ timeout=10,
+ text=True,
+ )
+ if not (Path(temp_dir) / f"{doc_path.stem}.html").exists():
+ error_msg = result.stderr.strip() or "Document validation failed"
+ print(f"Validation error: {error_msg}", file=sys.stderr)
+ return False
+ return True
+ except FileNotFoundError:
+ print("Warning: soffice not found. Skipping validation.", file=sys.stderr)
+ return True
+ except subprocess.TimeoutExpired:
+ print("Validation error: Timeout during conversion", file=sys.stderr)
+ return False
+ except Exception as e:
+ print(f"Validation error: {e}", file=sys.stderr)
+ return False
+
+
+def condense_xml(xml_file):
+ """Strip unnecessary whitespace and remove comments."""
+ with open(xml_file, "r", encoding="utf-8") as f:
+ dom = defusedxml.minidom.parse(f)
+
+ # Process each element to remove whitespace and comments
+ for element in dom.getElementsByTagName("*"):
+ # Skip w:t elements and their processing
+ if element.tagName.endswith(":t"):
+ continue
+
+ # Remove whitespace-only text nodes and comment nodes
+ for child in list(element.childNodes):
+ if (
+ child.nodeType == child.TEXT_NODE
+ and child.nodeValue
+ and child.nodeValue.strip() == ""
+ ) or child.nodeType == child.COMMENT_NODE:
+ element.removeChild(child)
+
+ # Write back the condensed XML
+ with open(xml_file, "wb") as f:
+ f.write(dom.toxml(encoding="UTF-8"))
+
+
+if __name__ == "__main__":
+ main()
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/scripts/unpack.py', '#!/usr/bin/env python3
+"""Unpack and format XML contents of Office files (.docx, .pptx, .xlsx)"""
+
+import random
+import sys
+import defusedxml.minidom
+import zipfile
+from pathlib import Path
+
+# Get command line arguments
+assert len(sys.argv) == 3, "Usage: python unpack.py "
+input_file, output_dir = sys.argv[1], sys.argv[2]
+
+# Extract and format
+output_path = Path(output_dir)
+output_path.mkdir(parents=True, exist_ok=True)
+zipfile.ZipFile(input_file).extractall(output_path)
+
+# Pretty print all XML files
+xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels"))
+for xml_file in xml_files:
+ content = xml_file.read_text(encoding="utf-8")
+ dom = defusedxml.minidom.parseString(content)
+ xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="ascii"))
+
+# For .docx files, suggest an RSID for tracked changes
+if input_file.endswith(".docx"):
+ suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8))
+ print(f"Suggested RSID for edit session: {suggested_rsid}")
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/scripts/validate.py', '#!/usr/bin/env python3
+"""
+Command line tool to validate Office document XML files against XSD schemas and tracked changes.
+
+Usage:
+ python validate.py --original
+"""
+
+import argparse
+import sys
+from pathlib import Path
+
+from validation import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Validate Office document XML files")
+ parser.add_argument(
+ "unpacked_dir",
+ help="Path to unpacked Office document directory",
+ )
+ parser.add_argument(
+ "--original",
+ required=True,
+ help="Path to original file (.docx/.pptx/.xlsx)",
+ )
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="Enable verbose output",
+ )
+ args = parser.parse_args()
+
+ # Validate paths
+ unpacked_dir = Path(args.unpacked_dir)
+ original_file = Path(args.original)
+ file_extension = original_file.suffix.lower()
+ assert unpacked_dir.is_dir(), f"Error: {unpacked_dir} is not a directory"
+ assert original_file.is_file(), f"Error: {original_file} is not a file"
+ assert file_extension in [".docx", ".pptx", ".xlsx"], (
+ f"Error: {original_file} must be a .docx, .pptx, or .xlsx file"
+ )
+
+ # Run validations
+ match file_extension:
+ case ".docx":
+ validators = [DOCXSchemaValidator, RedliningValidator]
+ case ".pptx":
+ validators = [PPTXSchemaValidator]
+ case _:
+ print(f"Error: Validation not supported for file type {file_extension}")
+ sys.exit(1)
+
+ # Run validators
+ success = True
+ for V in validators:
+ validator = V(unpacked_dir, original_file, verbose=args.verbose)
+ if not validator.validate():
+ success = False
+
+ if success:
+ print("All validations PASSED!")
+
+ sys.exit(0 if success else 1)
+
+
+if __name__ == "__main__":
+ main()
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/scripts/validation/__init__.py', '"""
+Validation modules for Word document processing.
+"""
+
+from .base import BaseSchemaValidator
+from .docx import DOCXSchemaValidator
+from .pptx import PPTXSchemaValidator
+from .redlining import RedliningValidator
+
+__all__ = [
+ "BaseSchemaValidator",
+ "DOCXSchemaValidator",
+ "PPTXSchemaValidator",
+ "RedliningValidator",
+]
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/scripts/validation/base.py', E'"""
+Base validator with common validation logic for document files.
+"""
+
+import re
+from pathlib import Path
+
+import lxml.etree
+
+
+class BaseSchemaValidator:
+ """Base validator with common validation logic for document files."""
+
+ # Elements whose ''id'' attributes must be unique within their file
+ # Format: element_name -> (attribute_name, scope)
+ # scope can be ''file'' (unique within file) or ''global'' (unique across all files)
+ UNIQUE_ID_REQUIREMENTS = {
+ # Word elements
+ "comment": ("id", "file"), # Comment IDs in comments.xml
+ "commentrangestart": ("id", "file"), # Must match comment IDs
+ "commentrangeend": ("id", "file"), # Must match comment IDs
+ "bookmarkstart": ("id", "file"), # Bookmark start IDs
+ "bookmarkend": ("id", "file"), # Bookmark end IDs
+ # Note: ins and del (track changes) can share IDs when part of same revision
+ # PowerPoint elements
+ "sldid": ("id", "file"), # Slide IDs in presentation.xml
+ "sldmasterid": ("id", "global"), # Slide master IDs must be globally unique
+ "sldlayoutid": ("id", "global"), # Slide layout IDs must be globally unique
+ "cm": ("authorid", "file"), # Comment author IDs
+ # Excel elements
+ "sheet": ("sheetid", "file"), # Sheet IDs in workbook.xml
+ "definedname": ("id", "file"), # Named range IDs
+ # Drawing/Shape elements (all formats)
+ "cxnsp": ("id", "file"), # Connection shape IDs
+ "sp": ("id", "file"), # Shape IDs
+ "pic": ("id", "file"), # Picture IDs
+ "grpsp": ("id", "file"), # Group shape IDs
+ }
+
+ # Mapping of element names to expected relationship types
+ # Subclasses should override this with format-specific mappings
+ ELEMENT_RELATIONSHIP_TYPES = {}
+
+ # Unified schema mappings for all Office document types
+ SCHEMA_MAPPINGS = {
+ # Document type specific schemas
+ "word": "ISO-IEC29500-4_2016/wml.xsd", # Word documents
+ "ppt": "ISO-IEC29500-4_2016/pml.xsd", # PowerPoint presentations
+ "xl": "ISO-IEC29500-4_2016/sml.xsd", # Excel spreadsheets
+ # Common file types
+ "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd",
+ "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd",
+ "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd",
+ "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd",
+ ".rels": "ecma/fouth-edition/opc-relationships.xsd",
+ # Word-specific files
+ "people.xml": "microsoft/wml-2012.xsd",
+ "commentsIds.xml": "microsoft/wml-cid-2016.xsd",
+ "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd",
+ "commentsExtended.xml": "microsoft/wml-2012.xsd",
+ # Chart files (common across document types)
+ "chart": "ISO-IEC29500-4_2016/dml-chart.xsd",
+ # Theme files (common across document types)
+ "theme": "ISO-IEC29500-4_2016/dml-main.xsd",
+ # Drawing and media files
+ "drawing": "ISO-IEC29500-4_2016/dml-main.xsd",
+ }
+
+ # Unified namespace constants
+ MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006"
+ XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
+
+ # Common OOXML namespaces used across validators
+ PACKAGE_RELATIONSHIPS_NAMESPACE = (
+ "http://schemas.openxmlformats.org/package/2006/relationships"
+ )
+ OFFICE_RELATIONSHIPS_NAMESPACE = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
+ )
+ CONTENT_TYPES_NAMESPACE = (
+ "http://schemas.openxmlformats.org/package/2006/content-types"
+ )
+
+ # Folders where we should clean ignorable namespaces
+ MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"}
+
+ # All allowed OOXML namespaces (superset of all document types)
+ OOXML_NAMESPACES = {
+ "http://schemas.openxmlformats.org/officeDocument/2006/math",
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
+ "http://schemas.openxmlformats.org/schemaLibrary/2006/main",
+ "http://schemas.openxmlformats.org/drawingml/2006/main",
+ "http://schemas.openxmlformats.org/drawingml/2006/chart",
+ "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing",
+ "http://schemas.openxmlformats.org/drawingml/2006/diagram",
+ "http://schemas.openxmlformats.org/drawingml/2006/picture",
+ "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing",
+ "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
+ "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
+ "http://schemas.openxmlformats.org/presentationml/2006/main",
+ "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
+ "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes",
+ "http://www.w3.org/XML/1998/namespace",
+ }
+
+ def __init__(self, unpacked_dir, original_file, verbose=False):
+ self.unpacked_dir = Path(unpacked_dir).resolve()
+ self.original_file = Path(original_file)
+ self.verbose = verbose
+
+ # Set schemas directory
+ self.schemas_dir = Path(__file__).parent.parent.parent / "schemas"
+
+ # Get all XML and .rels files
+ patterns = ["*.xml", "*.rels"]
+ self.xml_files = [
+ f for pattern in patterns for f in self.unpacked_dir.rglob(pattern)
+ ]
+
+ if not self.xml_files:
+ print(f"Warning: No XML files found in {self.unpacked_dir}")
+
+ def validate(self):
+ """Run all validation checks and return True if all pass."""
+ raise NotImplementedError("Subclasses must implement the validate method")
+
+ def validate_xml(self):
+ """Validate that all XML files are well-formed."""
+ errors = []
+
+ for xml_file in self.xml_files:
+ try:
+ # Try to parse the XML file
+ lxml.etree.parse(str(xml_file))
+ except lxml.etree.XMLSyntaxError as e:
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Line {e.lineno}: {e.msg}"
+ )
+ except Exception as e:
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Unexpected error: {str(e)}"
+ )
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} XML violations:")
+ for error in errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - All XML files are well-formed")
+ return True
+
+ def validate_namespaces(self):
+ """Validate that namespace prefixes in Ignorable attributes are declared."""
+ errors = []
+
+ for xml_file in self.xml_files:
+ try:
+ root = lxml.etree.parse(str(xml_file)).getroot()
+ declared = set(root.nsmap.keys()) - {None} # Exclude default namespace
+
+ for attr_val in [
+ v for k, v in root.attrib.items() if k.endswith("Ignorable")
+ ]:
+ undeclared = set(attr_val.split()) - declared
+ errors.extend(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Namespace ''{ns}'' in Ignorable but not declared"
+ for ns in undeclared
+ )
+ except lxml.etree.XMLSyntaxError:
+ continue
+
+ if errors:
+ print(f"FAILED - {len(errors)} namespace issues:")
+ for error in errors:
+ print(error)
+ return False
+ if self.verbose:
+ print("PASSED - All namespace prefixes properly declared")
+ return True
+
+ def validate_unique_ids(self):
+ """Validate that specific IDs are unique according to OOXML requirements."""
+ errors = []
+ global_ids = {} # Track globally unique IDs across all files
+
+ for xml_file in self.xml_files:
+ try:
+ root = lxml.etree.parse(str(xml_file)).getroot()
+ file_ids = {} # Track IDs that must be unique within this file
+
+ # Remove all mc:AlternateContent elements from the tree
+ mc_elements = root.xpath(
+ ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE}
+ )
+ for elem in mc_elements:
+ elem.getparent().remove(elem)
+
+ # Now check IDs in the cleaned tree
+ for elem in root.iter():
+ # Get the element name without namespace
+ tag = (
+ elem.tag.split("}")[-1].lower()
+ if "}" in elem.tag
+ else elem.tag.lower()
+ )
+
+ # Check if this element type has ID uniqueness requirements
+ if tag in self.UNIQUE_ID_REQUIREMENTS:
+ attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag]
+
+ # Look for the specified attribute
+ id_value = None
+ for attr, value in elem.attrib.items():
+ attr_local = (
+ attr.split("}")[-1].lower()
+ if "}" in attr
+ else attr.lower()
+ )
+ if attr_local == attr_name:
+ id_value = value
+ break
+
+ if id_value is not None:
+ if scope == "global":
+ # Check global uniqueness
+ if id_value in global_ids:
+ prev_file, prev_line, prev_tag = global_ids[
+ id_value
+ ]
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Line {elem.sourceline}: Global ID ''{id_value}'' in <{tag}> "
+ f"already used in {prev_file} at line {prev_line} in <{prev_tag}>"
+ )
+ else:
+ global_ids[id_value] = (
+ xml_file.relative_to(self.unpacked_dir),
+ elem.sourceline,
+ tag,
+ )
+ elif scope == "file":
+ # Check file-level uniqueness
+ key = (tag, attr_name)
+ if key not in file_ids:
+ file_ids[key] = {}
+
+ if id_value in file_ids[key]:
+ prev_line = file_ids[key][id_value]
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Line {elem.sourceline}: Duplicate {attr_name}=''{id_value}'' in <{tag}> "
+ f"(first occurrence at line {prev_line})"
+ )
+ else:
+ file_ids[key][id_value] = elem.sourceline
+
+ except (lxml.etree.XMLSyntaxError, Exception) as e:
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
+ )
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} ID uniqueness violations:")
+ for error in errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - All required IDs are unique")
+ return True
+
+ def validate_file_references(self):
+ """
+ Validate that all .rels files properly reference files and that all files are referenced.
+ """
+ errors = []
+
+ # Find all .rels files
+ rels_files = list(self.unpacked_dir.rglob("*.rels"))
+
+ if not rels_files:
+ if self.verbose:
+ print("PASSED - No .rels files found")
+ return True
+
+ # Get all files in the unpacked directory (excluding reference files)
+ all_files = []
+ for file_path in self.unpacked_dir.rglob("*"):
+ if (
+ file_path.is_file()
+ and file_path.name != "[Content_Types].xml"
+ and not file_path.name.endswith(".rels")
+ ): # This file is not referenced by .rels
+ all_files.append(file_path.resolve())
+
+ # Track all files that are referenced by any .rels file
+ all_referenced_files = set()
+
+ if self.verbose:
+ print(
+ f"Found {len(rels_files)} .rels files and {len(all_files)} target files"
+ )
+
+ # Check each .rels file
+ for rels_file in rels_files:
+ try:
+ # Parse relationships file
+ rels_root = lxml.etree.parse(str(rels_file)).getroot()
+
+ # Get the directory where this .rels file is located
+ rels_dir = rels_file.parent
+
+ # Find all relationships and their targets
+ referenced_files = set()
+ broken_refs = []
+
+ for rel in rels_root.findall(
+ ".//ns:Relationship",
+ namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE},
+ ):
+ target = rel.get("Target")
+ if target and not target.startswith(
+ ("http", "mailto:")
+ ): # Skip external URLs
+ # Resolve the target path relative to the .rels file location
+ if rels_file.name == ".rels":
+ # Root .rels file - targets are relative to unpacked_dir
+ target_path = self.unpacked_dir / target
+ else:
+ # Other .rels files - targets are relative to their parent''s parent
+ # e.g., word/_rels/document.xml.rels -> targets relative to word/
+ base_dir = rels_dir.parent
+ target_path = base_dir / target
+
+ # Normalize the path and check if it exists
+ try:
+ target_path = target_path.resolve()
+ if target_path.exists() and target_path.is_file():
+ referenced_files.add(target_path)
+ all_referenced_files.add(target_path)
+ else:
+ broken_refs.append((target, rel.sourceline))
+ except (OSError, ValueError):
+ broken_refs.append((target, rel.sourceline))
+
+ # Report broken references
+ if broken_refs:
+ rel_path = rels_file.relative_to(self.unpacked_dir)
+ for broken_ref, line_num in broken_refs:
+ errors.append(
+ f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}"
+ )
+
+ except Exception as e:
+ rel_path = rels_file.relative_to(self.unpacked_dir)
+ errors.append(f" Error parsing {rel_path}: {e}")
+
+ # Check for unreferenced files (files that exist but are not referenced anywhere)
+ unreferenced_files = set(all_files) - all_referenced_files
+
+ if unreferenced_files:
+ for unref_file in sorted(unreferenced_files):
+ unref_rel_path = unref_file.relative_to(self.unpacked_dir)
+ errors.append(f" Unreferenced file: {unref_rel_path}")
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} relationship validation errors:")
+ for error in errors:
+ print(error)
+ print(
+ "CRITICAL: These errors will cause the document to appear corrupt. "
+ + "Broken references MUST be fixed, "
+ + "and unreferenced files MUST be referenced or removed."
+ )
+ return False
+ else:
+ if self.verbose:
+ print(
+ "PASSED - All references are valid and all files are properly referenced"
+ )
+ return True
+
+ def validate_all_relationship_ids(self):
+ """
+ Validate that all r:id attributes in XML files reference existing IDs
+ in their corresponding .rels files, and optionally validate relationship types.
+ """
+ import lxml.etree
+
+ errors = []
+
+ # Process each XML file that might contain r:id references
+ for xml_file in self.xml_files:
+ # Skip .rels files themselves
+ if xml_file.suffix == ".rels":
+ continue
+
+ # Determine the corresponding .rels file
+ # For dir/file.xml, it''s dir/_rels/file.xml.rels
+ rels_dir = xml_file.parent / "_rels"
+ rels_file = rels_dir / f"{xml_file.name}.rels"
+
+ # Skip if there''s no corresponding .rels file (that''s okay)
+ if not rels_file.exists():
+ continue
+
+ try:
+ # Parse the .rels file to get valid relationship IDs and their types
+ rels_root = lxml.etree.parse(str(rels_file)).getroot()
+ rid_to_type = {}
+
+ for rel in rels_root.findall(
+ f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
+ ):
+ rid = rel.get("Id")
+ rel_type = rel.get("Type", "")
+ if rid:
+ # Check for duplicate rIds
+ if rid in rid_to_type:
+ rels_rel_path = rels_file.relative_to(self.unpacked_dir)
+ errors.append(
+ f" {rels_rel_path}: Line {rel.sourceline}: "
+ f"Duplicate relationship ID ''{rid}'' (IDs must be unique)"
+ )
+ # Extract just the type name from the full URL
+ type_name = (
+ rel_type.split("/")[-1] if "/" in rel_type else rel_type
+ )
+ rid_to_type[rid] = type_name
+
+ # Parse the XML file to find all r:id references
+ xml_root = lxml.etree.parse(str(xml_file)).getroot()
+
+ # Find all elements with r:id attributes
+ for elem in xml_root.iter():
+ # Check for r:id attribute (relationship ID)
+ rid_attr = elem.get(f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id")
+ if rid_attr:
+ xml_rel_path = xml_file.relative_to(self.unpacked_dir)
+ elem_name = (
+ elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
+ )
+
+ # Check if the ID exists
+ if rid_attr not in rid_to_type:
+ errors.append(
+ f" {xml_rel_path}: Line {elem.sourceline}: "
+ f"<{elem_name}> references non-existent relationship ''{rid_attr}'' "
+ f"(valid IDs: {'', ''.join(sorted(rid_to_type.keys())[:5])}{''...'' if len(rid_to_type) > 5 else ''''})"
+ )
+ # Check if we have type expectations for this element
+ elif self.ELEMENT_RELATIONSHIP_TYPES:
+ expected_type = self._get_expected_relationship_type(
+ elem_name
+ )
+ if expected_type:
+ actual_type = rid_to_type[rid_attr]
+ # Check if the actual type matches or contains the expected type
+ if expected_type not in actual_type.lower():
+ errors.append(
+ f" {xml_rel_path}: Line {elem.sourceline}: "
+ f"<{elem_name}> references ''{rid_attr}'' which points to ''{actual_type}'' "
+ f"but should point to a ''{expected_type}'' relationship"
+ )
+
+ except Exception as e:
+ xml_rel_path = xml_file.relative_to(self.unpacked_dir)
+ errors.append(f" Error processing {xml_rel_path}: {e}")
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} relationship ID reference errors:")
+ for error in errors:
+ print(error)
+ print("\\nThese ID mismatches will cause the document to appear corrupt!")
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - All relationship ID references are valid")
+ return True
+
+ def _get_expected_relationship_type(self, element_name):
+ """
+ Get the expected relationship type for an element.
+ First checks the explicit mapping, then tries pattern detection.
+ """
+ # Normalize element name to lowercase
+ elem_lower = element_name.lower()
+
+ # Check explicit mapping first
+ if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES:
+ return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower]
+
+ # Try pattern detection for common patterns
+ # Pattern 1: Elements ending in "Id" often expect a relationship of the prefix type
+ if elem_lower.endswith("id") and len(elem_lower) > 2:
+ # e.g., "sldId" -> "sld", "sldMasterId" -> "sldMaster"
+ prefix = elem_lower[:-2] # Remove "id"
+ # Check if this might be a compound like "sldMasterId"
+ if prefix.endswith("master"):
+ return prefix.lower()
+ elif prefix.endswith("layout"):
+ return prefix.lower()
+ else:
+ # Simple case like "sldId" -> "slide"
+ # Common transformations
+ if prefix == "sld":
+ return "slide"
+ return prefix.lower()
+
+ # Pattern 2: Elements ending in "Reference" expect a relationship of the prefix type
+ if elem_lower.endswith("reference") and len(elem_lower) > 9:
+ prefix = elem_lower[:-9] # Remove "reference"
+ return prefix.lower()
+
+ return None
+
+ def validate_content_types(self):
+ """Validate that all content files are properly declared in [Content_Types].xml."""
+ errors = []
+
+ # Find [Content_Types].xml file
+ content_types_file = self.unpacked_dir / "[Content_Types].xml"
+ if not content_types_file.exists():
+ print("FAILED - [Content_Types].xml file not found")
+ return False
+
+ try:
+ # Parse and get all declared parts and extensions
+ root = lxml.etree.parse(str(content_types_file)).getroot()
+ declared_parts = set()
+ declared_extensions = set()
+
+ # Get Override declarations (specific files)
+ for override in root.findall(
+ f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override"
+ ):
+ part_name = override.get("PartName")
+ if part_name is not None:
+ declared_parts.add(part_name.lstrip("/"))
+
+ # Get Default declarations (by extension)
+ for default in root.findall(
+ f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default"
+ ):
+ extension = default.get("Extension")
+ if extension is not None:
+ declared_extensions.add(extension.lower())
+
+ # Root elements that require content type declaration
+ declarable_roots = {
+ "sld",
+ "sldLayout",
+ "sldMaster",
+ "presentation", # PowerPoint
+ "document", # Word
+ "workbook",
+ "worksheet", # Excel
+ "theme", # Common
+ }
+
+ # Common media file extensions that should be declared
+ media_extensions = {
+ "png": "image/png",
+ "jpg": "image/jpeg",
+ "jpeg": "image/jpeg",
+ "gif": "image/gif",
+ "bmp": "image/bmp",
+ "tiff": "image/tiff",
+ "wmf": "image/x-wmf",
+ "emf": "image/x-emf",
+ }
+
+ # Get all files in the unpacked directory
+ all_files = list(self.unpacked_dir.rglob("*"))
+ all_files = [f for f in all_files if f.is_file()]
+
+ # Check all XML files for Override declarations
+ for xml_file in self.xml_files:
+ path_str = str(xml_file.relative_to(self.unpacked_dir)).replace(
+ "\\\\", "/"
+ )
+
+ # Skip non-content files
+ if any(
+ skip in path_str
+ for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"]
+ ):
+ continue
+
+ try:
+ root_tag = lxml.etree.parse(str(xml_file)).getroot().tag
+ root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag
+
+ if root_name in declarable_roots and path_str not in declared_parts:
+ errors.append(
+ f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml"
+ )
+
+ except Exception:
+ continue # Skip unparseable files
+
+ # Check all non-XML files for Default extension declarations
+ for file_path in all_files:
+ # Skip XML files and metadata files (already checked above)
+ if file_path.suffix.lower() in {".xml", ".rels"}:
+ continue
+ if file_path.name == "[Content_Types].xml":
+ continue
+ if "_rels" in file_path.parts or "docProps" in file_path.parts:
+ continue
+
+ extension = file_path.suffix.lstrip(".").lower()
+ if extension and extension not in declared_extensions:
+ # Check if it''s a known media extension that should be declared
+ if extension in media_extensions:
+ relative_path = file_path.relative_to(self.unpacked_dir)
+ errors.append(
+ f'' {relative_path}: File with extension \\''{extension}\\'' not declared in [Content_Types].xml - should add: ''
+ )
+
+ except Exception as e:
+ errors.append(f" Error parsing [Content_Types].xml: {e}")
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} content type declaration errors:")
+ for error in errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print(
+ "PASSED - All content files are properly declared in [Content_Types].xml"
+ )
+ return True
+
+ def validate_file_against_xsd(self, xml_file, verbose=False):
+ """Validate a single XML file against XSD schema, comparing with original.
+
+ Args:
+ xml_file: Path to XML file to validate
+ verbose: Enable verbose output
+
+ Returns:
+ tuple: (is_valid, new_errors_set) where is_valid is True/False/None (skipped)
+ """
+ # Resolve both paths to handle symlinks
+ xml_file = Path(xml_file).resolve()
+ unpacked_dir = self.unpacked_dir.resolve()
+
+ # Validate current file
+ is_valid, current_errors = self._validate_single_file_xsd(
+ xml_file, unpacked_dir
+ )
+
+ if is_valid is None:
+ return None, set() # Skipped
+ elif is_valid:
+ return True, set() # Valid, no errors
+
+ # Get errors from original file for this specific file
+ original_errors = self._get_original_file_errors(xml_file)
+
+ # Compare with original (both are guaranteed to be sets here)
+ assert current_errors is not None
+ new_errors = current_errors - original_errors
+
+ if new_errors:
+ if verbose:
+ relative_path = xml_file.relative_to(unpacked_dir)
+ print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)")
+ for error in list(new_errors)[:3]:
+ truncated = error[:250] + "..." if len(error) > 250 else error
+ print(f" - {truncated}")
+ return False, new_errors
+ else:
+ # All errors existed in original
+ if verbose:
+ print(
+ f"PASSED - No new errors (original had {len(current_errors)} errors)"
+ )
+ return True, set()
+
+ def validate_against_xsd(self):
+ """Validate XML files against XSD schemas, showing only new errors compared to original."""
+ new_errors = []
+ original_error_count = 0
+ valid_count = 0
+ skipped_count = 0
+
+ for xml_file in self.xml_files:
+ relative_path = str(xml_file.relative_to(self.unpacked_dir))
+ is_valid, new_file_errors = self.validate_file_against_xsd(
+ xml_file, verbose=False
+ )
+
+ if is_valid is None:
+ skipped_count += 1
+ continue
+ elif is_valid and not new_file_errors:
+ valid_count += 1
+ continue
+ elif is_valid:
+ # Had errors but all existed in original
+ original_error_count += 1
+ valid_count += 1
+ continue
+
+ # Has new errors
+ new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)")
+ for error in list(new_file_errors)[:3]: # Show first 3 errors
+ new_errors.append(
+ f" - {error[:250]}..." if len(error) > 250 else f" - {error}"
+ )
+
+ # Print summary
+ if self.verbose:
+ print(f"Validated {len(self.xml_files)} files:")
+ print(f" - Valid: {valid_count}")
+ print(f" - Skipped (no schema): {skipped_count}")
+ if original_error_count:
+ print(f" - With original errors (ignored): {original_error_count}")
+ print(
+ f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith('' '')]) or 0}"
+ )
+
+ if new_errors:
+ print("\\nFAILED - Found NEW validation errors:")
+ for error in new_errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print("\\nPASSED - No new XSD validation errors introduced")
+ return True
+
+ def _get_schema_path(self, xml_file):
+ """Determine the appropriate schema path for an XML file."""
+ # Check exact filename match
+ if xml_file.name in self.SCHEMA_MAPPINGS:
+ return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name]
+
+ # Check .rels files
+ if xml_file.suffix == ".rels":
+ return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"]
+
+ # Check chart files
+ if "charts/" in str(xml_file) and xml_file.name.startswith("chart"):
+ return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"]
+
+ # Check theme files
+ if "theme/" in str(xml_file) and xml_file.name.startswith("theme"):
+ return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"]
+
+ # Check if file is in a main content folder and use appropriate schema
+ if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS:
+ return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name]
+
+ return None
+
+ def _clean_ignorable_namespaces(self, xml_doc):
+ """Remove attributes and elements not in allowed namespaces."""
+ # Create a clean copy
+ xml_string = lxml.etree.tostring(xml_doc, encoding="unicode")
+ xml_copy = lxml.etree.fromstring(xml_string)
+
+ # Remove attributes not in allowed namespaces
+ for elem in xml_copy.iter():
+ attrs_to_remove = []
+
+ for attr in elem.attrib:
+ # Check if attribute is from a namespace other than allowed ones
+ if "{" in attr:
+ ns = attr.split("}")[0][1:]
+ if ns not in self.OOXML_NAMESPACES:
+ attrs_to_remove.append(attr)
+
+ # Remove collected attributes
+ for attr in attrs_to_remove:
+ del elem.attrib[attr]
+
+ # Remove elements not in allowed namespaces
+ self._remove_ignorable_elements(xml_copy)
+
+ return lxml.etree.ElementTree(xml_copy)
+
+ def _remove_ignorable_elements(self, root):
+ """Recursively remove all elements not in allowed namespaces."""
+ elements_to_remove = []
+
+ # Find elements to remove
+ for elem in list(root):
+ # Skip non-element nodes (comments, processing instructions, etc.)
+ if not hasattr(elem, "tag") or callable(elem.tag):
+ continue
+
+ tag_str = str(elem.tag)
+ if tag_str.startswith("{"):
+ ns = tag_str.split("}")[0][1:]
+ if ns not in self.OOXML_NAMESPACES:
+ elements_to_remove.append(elem)
+ continue
+
+ # Recursively clean child elements
+ self._remove_ignorable_elements(elem)
+
+ # Remove collected elements
+ for elem in elements_to_remove:
+ root.remove(elem)
+
+ def _preprocess_for_mc_ignorable(self, xml_doc):
+ """Preprocess XML to handle mc:Ignorable attribute properly."""
+ # Remove mc:Ignorable attributes before validation
+ root = xml_doc.getroot()
+
+ # Remove mc:Ignorable attribute from root
+ if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib:
+ del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"]
+
+ return xml_doc
+
+ def _validate_single_file_xsd(self, xml_file, base_path):
+ """Validate a single XML file against XSD schema. Returns (is_valid, errors_set)."""
+ schema_path = self._get_schema_path(xml_file)
+ if not schema_path:
+ return None, None # Skip file
+
+ try:
+ # Load schema
+ with open(schema_path, "rb") as xsd_file:
+ parser = lxml.etree.XMLParser()
+ xsd_doc = lxml.etree.parse(
+ xsd_file, parser=parser, base_url=str(schema_path)
+ )
+ schema = lxml.etree.XMLSchema(xsd_doc)
+
+ # Load and preprocess XML
+ with open(xml_file, "r") as f:
+ xml_doc = lxml.etree.parse(f)
+
+ xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc)
+ xml_doc = self._preprocess_for_mc_ignorable(xml_doc)
+
+ # Clean ignorable namespaces if needed
+ relative_path = xml_file.relative_to(base_path)
+ if (
+ relative_path.parts
+ and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS
+ ):
+ xml_doc = self._clean_ignorable_namespaces(xml_doc)
+
+ # Validate
+ if schema.validate(xml_doc):
+ return True, set()
+ else:
+ errors = set()
+ for error in schema.error_log:
+ # Store normalized error message (without line numbers for comparison)
+ errors.add(error.message)
+ return False, errors
+
+ except Exception as e:
+ return False, {str(e)}
+
+ def _get_original_file_errors(self, xml_file):
+ """Get XSD validation errors from a single file in the original document.
+
+ Args:
+ xml_file: Path to the XML file in unpacked_dir to check
+
+ Returns:
+ set: Set of error messages from the original file
+ """
+ import tempfile
+ import zipfile
+
+ # Resolve both paths to handle symlinks (e.g., /var vs /private/var on macOS)
+ xml_file = Path(xml_file).resolve()
+ unpacked_dir = self.unpacked_dir.resolve()
+ relative_path = xml_file.relative_to(unpacked_dir)
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_path = Path(temp_dir)
+
+ # Extract original file
+ with zipfile.ZipFile(self.original_file, "r") as zip_ref:
+ zip_ref.extractall(temp_path)
+
+ # Find corresponding file in original
+ original_xml_file = temp_path / relative_path
+
+ if not original_xml_file.exists():
+ # File didn''t exist in original, so no original errors
+ return set()
+
+ # Validate the specific file in original
+ is_valid, errors = self._validate_single_file_xsd(
+ original_xml_file, temp_path
+ )
+ return errors if errors else set()
+
+ def _remove_template_tags_from_text_nodes(self, xml_doc):
+ """Remove template tags from XML text nodes and collect warnings.
+
+ Template tags follow the pattern {{ ... }} and are used as placeholders
+ for content replacement. They should be removed from text content before
+ XSD validation while preserving XML structure.
+
+ Returns:
+ tuple: (cleaned_xml_doc, warnings_list)
+ """
+ warnings = []
+ template_pattern = re.compile(r"\\{\\{[^}]*\\}\\}")
+
+ # Create a copy of the document to avoid modifying the original
+ xml_string = lxml.etree.tostring(xml_doc, encoding="unicode")
+ xml_copy = lxml.etree.fromstring(xml_string)
+
+ def process_text_content(text, content_type):
+ if not text:
+ return text
+ matches = list(template_pattern.finditer(text))
+ if matches:
+ for match in matches:
+ warnings.append(
+ f"Found template tag in {content_type}: {match.group()}"
+ )
+ return template_pattern.sub("", text)
+ return text
+
+ # Process all text nodes in the document
+ for elem in xml_copy.iter():
+ # Skip processing if this is a w:t element
+ if not hasattr(elem, "tag") or callable(elem.tag):
+ continue
+ tag_str = str(elem.tag)
+ if tag_str.endswith("}t") or tag_str == "t":
+ continue
+
+ elem.text = process_text_content(elem.text, "text content")
+ elem.tail = process_text_content(elem.tail, "tail content")
+
+ return lxml.etree.ElementTree(xml_copy), warnings
+
+
+if __name__ == "__main__":
+ raise RuntimeError("This module should not be run directly.")
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/scripts/validation/docx.py', E'"""
+Validator for Word document XML files against XSD schemas.
+"""
+
+import re
+import tempfile
+import zipfile
+
+import lxml.etree
+
+from .base import BaseSchemaValidator
+
+
+class DOCXSchemaValidator(BaseSchemaValidator):
+ """Validator for Word document XML files against XSD schemas."""
+
+ # Word-specific namespace
+ WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
+
+ # Word-specific element to relationship type mappings
+ # Start with empty mapping - add specific cases as we discover them
+ ELEMENT_RELATIONSHIP_TYPES = {}
+
+ def validate(self):
+ """Run all validation checks and return True if all pass."""
+ # Test 0: XML well-formedness
+ if not self.validate_xml():
+ return False
+
+ # Test 1: Namespace declarations
+ all_valid = True
+ if not self.validate_namespaces():
+ all_valid = False
+
+ # Test 2: Unique IDs
+ if not self.validate_unique_ids():
+ all_valid = False
+
+ # Test 3: Relationship and file reference validation
+ if not self.validate_file_references():
+ all_valid = False
+
+ # Test 4: Content type declarations
+ if not self.validate_content_types():
+ all_valid = False
+
+ # Test 5: XSD schema validation
+ if not self.validate_against_xsd():
+ all_valid = False
+
+ # Test 6: Whitespace preservation
+ if not self.validate_whitespace_preservation():
+ all_valid = False
+
+ # Test 7: Deletion validation
+ if not self.validate_deletions():
+ all_valid = False
+
+ # Test 8: Insertion validation
+ if not self.validate_insertions():
+ all_valid = False
+
+ # Test 9: Relationship ID reference validation
+ if not self.validate_all_relationship_ids():
+ all_valid = False
+
+ # Count and compare paragraphs
+ self.compare_paragraph_counts()
+
+ return all_valid
+
+ def validate_whitespace_preservation(self):
+ """
+ Validate that w:t elements with whitespace have xml:space=''preserve''.
+ """
+ errors = []
+
+ for xml_file in self.xml_files:
+ # Only check document.xml files
+ if xml_file.name != "document.xml":
+ continue
+
+ try:
+ root = lxml.etree.parse(str(xml_file)).getroot()
+
+ # Find all w:t elements
+ for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"):
+ if elem.text:
+ text = elem.text
+ # Check if text starts or ends with whitespace
+ if re.match(r"^\\s.*", text) or re.match(r".*\\s$", text):
+ # Check if xml:space="preserve" attribute exists
+ xml_space_attr = f"{{{self.XML_NAMESPACE}}}space"
+ if (
+ xml_space_attr not in elem.attrib
+ or elem.attrib[xml_space_attr] != "preserve"
+ ):
+ # Show a preview of the text
+ text_preview = (
+ repr(text)[:50] + "..."
+ if len(repr(text)) > 50
+ else repr(text)
+ )
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Line {elem.sourceline}: w:t element with whitespace missing xml:space=''preserve'': {text_preview}"
+ )
+
+ except (lxml.etree.XMLSyntaxError, Exception) as e:
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
+ )
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} whitespace preservation violations:")
+ for error in errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - All whitespace is properly preserved")
+ return True
+
+ def validate_deletions(self):
+ """
+ Validate that w:t elements are not within w:del elements.
+ For some reason, XSD validation does not catch this, so we do it manually.
+ """
+ errors = []
+
+ for xml_file in self.xml_files:
+ # Only check document.xml files
+ if xml_file.name != "document.xml":
+ continue
+
+ try:
+ root = lxml.etree.parse(str(xml_file)).getroot()
+
+ # Find all w:t elements that are descendants of w:del elements
+ namespaces = {"w": self.WORD_2006_NAMESPACE}
+ xpath_expression = ".//w:del//w:t"
+ problematic_t_elements = root.xpath(
+ xpath_expression, namespaces=namespaces
+ )
+ for t_elem in problematic_t_elements:
+ if t_elem.text:
+ # Show a preview of the text
+ text_preview = (
+ repr(t_elem.text)[:50] + "..."
+ if len(repr(t_elem.text)) > 50
+ else repr(t_elem.text)
+ )
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Line {t_elem.sourceline}: found within : {text_preview}"
+ )
+
+ except (lxml.etree.XMLSyntaxError, Exception) as e:
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
+ )
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} deletion validation violations:")
+ for error in errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - No w:t elements found within w:del elements")
+ return True
+
+ def count_paragraphs_in_unpacked(self):
+ """Count the number of paragraphs in the unpacked document."""
+ count = 0
+
+ for xml_file in self.xml_files:
+ # Only check document.xml files
+ if xml_file.name != "document.xml":
+ continue
+
+ try:
+ root = lxml.etree.parse(str(xml_file)).getroot()
+ # Count all w:p elements
+ paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
+ count = len(paragraphs)
+ except Exception as e:
+ print(f"Error counting paragraphs in unpacked document: {e}")
+
+ return count
+
+ def count_paragraphs_in_original(self):
+ """Count the number of paragraphs in the original docx file."""
+ count = 0
+
+ try:
+ # Create temporary directory to unpack original
+ with tempfile.TemporaryDirectory() as temp_dir:
+ # Unpack original docx
+ with zipfile.ZipFile(self.original_file, "r") as zip_ref:
+ zip_ref.extractall(temp_dir)
+
+ # Parse document.xml
+ doc_xml_path = temp_dir + "/word/document.xml"
+ root = lxml.etree.parse(doc_xml_path).getroot()
+
+ # Count all w:p elements
+ paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
+ count = len(paragraphs)
+
+ except Exception as e:
+ print(f"Error counting paragraphs in original document: {e}")
+
+ return count
+
+ def validate_insertions(self):
+ """
+ Validate that w:delText elements are not within w:ins elements.
+ w:delText is only allowed in w:ins if nested within a w:del.
+ """
+ errors = []
+
+ for xml_file in self.xml_files:
+ if xml_file.name != "document.xml":
+ continue
+
+ try:
+ root = lxml.etree.parse(str(xml_file)).getroot()
+ namespaces = {"w": self.WORD_2006_NAMESPACE}
+
+ # Find w:delText in w:ins that are NOT within w:del
+ invalid_elements = root.xpath(
+ ".//w:ins//w:delText[not(ancestor::w:del)]",
+ namespaces=namespaces
+ )
+
+ for elem in invalid_elements:
+ text_preview = (
+ repr(elem.text or "")[:50] + "..."
+ if len(repr(elem.text or "")) > 50
+ else repr(elem.text or "")
+ )
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Line {elem.sourceline}: within : {text_preview}"
+ )
+
+ except (lxml.etree.XMLSyntaxError, Exception) as e:
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
+ )
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} insertion validation violations:")
+ for error in errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - No w:delText elements within w:ins elements")
+ return True
+
+ def compare_paragraph_counts(self):
+ """Compare paragraph counts between original and new document."""
+ original_count = self.count_paragraphs_in_original()
+ new_count = self.count_paragraphs_in_unpacked()
+
+ diff = new_count - original_count
+ diff_str = f"+{diff}" if diff > 0 else str(diff)
+ print(f"\\nParagraphs: {original_count} → {new_count} ({diff_str})")
+
+
+if __name__ == "__main__":
+ raise RuntimeError("This module should not be run directly.")
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/scripts/validation/pptx.py', E'"""
+Validator for PowerPoint presentation XML files against XSD schemas.
+"""
+
+import re
+
+from .base import BaseSchemaValidator
+
+
+class PPTXSchemaValidator(BaseSchemaValidator):
+ """Validator for PowerPoint presentation XML files against XSD schemas."""
+
+ # PowerPoint presentation namespace
+ PRESENTATIONML_NAMESPACE = (
+ "http://schemas.openxmlformats.org/presentationml/2006/main"
+ )
+
+ # PowerPoint-specific element to relationship type mappings
+ ELEMENT_RELATIONSHIP_TYPES = {
+ "sldid": "slide",
+ "sldmasterid": "slidemaster",
+ "notesmasterid": "notesmaster",
+ "sldlayoutid": "slidelayout",
+ "themeid": "theme",
+ "tablestyleid": "tablestyles",
+ }
+
+ def validate(self):
+ """Run all validation checks and return True if all pass."""
+ # Test 0: XML well-formedness
+ if not self.validate_xml():
+ return False
+
+ # Test 1: Namespace declarations
+ all_valid = True
+ if not self.validate_namespaces():
+ all_valid = False
+
+ # Test 2: Unique IDs
+ if not self.validate_unique_ids():
+ all_valid = False
+
+ # Test 3: UUID ID validation
+ if not self.validate_uuid_ids():
+ all_valid = False
+
+ # Test 4: Relationship and file reference validation
+ if not self.validate_file_references():
+ all_valid = False
+
+ # Test 5: Slide layout ID validation
+ if not self.validate_slide_layout_ids():
+ all_valid = False
+
+ # Test 6: Content type declarations
+ if not self.validate_content_types():
+ all_valid = False
+
+ # Test 7: XSD schema validation
+ if not self.validate_against_xsd():
+ all_valid = False
+
+ # Test 8: Notes slide reference validation
+ if not self.validate_notes_slide_references():
+ all_valid = False
+
+ # Test 9: Relationship ID reference validation
+ if not self.validate_all_relationship_ids():
+ all_valid = False
+
+ # Test 10: Duplicate slide layout references validation
+ if not self.validate_no_duplicate_slide_layouts():
+ all_valid = False
+
+ return all_valid
+
+ def validate_uuid_ids(self):
+ """Validate that ID attributes that look like UUIDs contain only hex values."""
+ import lxml.etree
+
+ errors = []
+ # UUID pattern: 8-4-4-4-12 hex digits with optional braces/hyphens
+ uuid_pattern = re.compile(
+ r"^[\\{\\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\\}\\)]?$"
+ )
+
+ for xml_file in self.xml_files:
+ try:
+ root = lxml.etree.parse(str(xml_file)).getroot()
+
+ # Check all elements for ID attributes
+ for elem in root.iter():
+ for attr, value in elem.attrib.items():
+ # Check if this is an ID attribute
+ attr_name = attr.split("}")[-1].lower()
+ if attr_name == "id" or attr_name.endswith("id"):
+ # Check if value looks like a UUID (has the right length and pattern structure)
+ if self._looks_like_uuid(value):
+ # Validate that it contains only hex characters in the right positions
+ if not uuid_pattern.match(value):
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: "
+ f"Line {elem.sourceline}: ID ''{value}'' appears to be a UUID but contains invalid hex characters"
+ )
+
+ except (lxml.etree.XMLSyntaxError, Exception) as e:
+ errors.append(
+ f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
+ )
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} UUID ID validation errors:")
+ for error in errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - All UUID-like IDs contain valid hex values")
+ return True
+
+ def _looks_like_uuid(self, value):
+ """Check if a value has the general structure of a UUID."""
+ # Remove common UUID delimiters
+ clean_value = value.strip("{}()").replace("-", "")
+ # Check if it''s 32 hex-like characters (could include invalid hex chars)
+ return len(clean_value) == 32 and all(c.isalnum() for c in clean_value)
+
+ def validate_slide_layout_ids(self):
+ """Validate that sldLayoutId elements in slide masters reference valid slide layouts."""
+ import lxml.etree
+
+ errors = []
+
+ # Find all slide master files
+ slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml"))
+
+ if not slide_masters:
+ if self.verbose:
+ print("PASSED - No slide masters found")
+ return True
+
+ for slide_master in slide_masters:
+ try:
+ # Parse the slide master file
+ root = lxml.etree.parse(str(slide_master)).getroot()
+
+ # Find the corresponding _rels file for this slide master
+ rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels"
+
+ if not rels_file.exists():
+ errors.append(
+ f" {slide_master.relative_to(self.unpacked_dir)}: "
+ f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}"
+ )
+ continue
+
+ # Parse the relationships file
+ rels_root = lxml.etree.parse(str(rels_file)).getroot()
+
+ # Build a set of valid relationship IDs that point to slide layouts
+ valid_layout_rids = set()
+ for rel in rels_root.findall(
+ f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
+ ):
+ rel_type = rel.get("Type", "")
+ if "slideLayout" in rel_type:
+ valid_layout_rids.add(rel.get("Id"))
+
+ # Find all sldLayoutId elements in the slide master
+ for sld_layout_id in root.findall(
+ f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId"
+ ):
+ r_id = sld_layout_id.get(
+ f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id"
+ )
+ layout_id = sld_layout_id.get("id")
+
+ if r_id and r_id not in valid_layout_rids:
+ errors.append(
+ f" {slide_master.relative_to(self.unpacked_dir)}: "
+ f"Line {sld_layout_id.sourceline}: sldLayoutId with id=''{layout_id}'' "
+ f"references r:id=''{r_id}'' which is not found in slide layout relationships"
+ )
+
+ except (lxml.etree.XMLSyntaxError, Exception) as e:
+ errors.append(
+ f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}"
+ )
+
+ if errors:
+ print(f"FAILED - Found {len(errors)} slide layout ID validation errors:")
+ for error in errors:
+ print(error)
+ print(
+ "Remove invalid references or add missing slide layouts to the relationships file."
+ )
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - All slide layout IDs reference valid slide layouts")
+ return True
+
+ def validate_no_duplicate_slide_layouts(self):
+ """Validate that each slide has exactly one slideLayout reference."""
+ import lxml.etree
+
+ errors = []
+ slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
+
+ for rels_file in slide_rels_files:
+ try:
+ root = lxml.etree.parse(str(rels_file)).getroot()
+
+ # Find all slideLayout relationships
+ layout_rels = [
+ rel
+ for rel in root.findall(
+ f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
+ )
+ if "slideLayout" in rel.get("Type", "")
+ ]
+
+ if len(layout_rels) > 1:
+ errors.append(
+ f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references"
+ )
+
+ except Exception as e:
+ errors.append(
+ f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
+ )
+
+ if errors:
+ print("FAILED - Found slides with duplicate slideLayout references:")
+ for error in errors:
+ print(error)
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - All slides have exactly one slideLayout reference")
+ return True
+
+ def validate_notes_slide_references(self):
+ """Validate that each notesSlide file is referenced by only one slide."""
+ import lxml.etree
+
+ errors = []
+ notes_slide_references = {} # Track which slides reference each notesSlide
+
+ # Find all slide relationship files
+ slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
+
+ if not slide_rels_files:
+ if self.verbose:
+ print("PASSED - No slide relationship files found")
+ return True
+
+ for rels_file in slide_rels_files:
+ try:
+ # Parse the relationships file
+ root = lxml.etree.parse(str(rels_file)).getroot()
+
+ # Find all notesSlide relationships
+ for rel in root.findall(
+ f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
+ ):
+ rel_type = rel.get("Type", "")
+ if "notesSlide" in rel_type:
+ target = rel.get("Target", "")
+ if target:
+ # Normalize the target path to handle relative paths
+ normalized_target = target.replace("../", "")
+
+ # Track which slide references this notesSlide
+ slide_name = rels_file.stem.replace(
+ ".xml", ""
+ ) # e.g., "slide1"
+
+ if normalized_target not in notes_slide_references:
+ notes_slide_references[normalized_target] = []
+ notes_slide_references[normalized_target].append(
+ (slide_name, rels_file)
+ )
+
+ except (lxml.etree.XMLSyntaxError, Exception) as e:
+ errors.append(
+ f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
+ )
+
+ # Check for duplicate references
+ for target, references in notes_slide_references.items():
+ if len(references) > 1:
+ slide_names = [ref[0] for ref in references]
+ errors.append(
+ f" Notes slide ''{target}'' is referenced by multiple slides: {'', ''.join(slide_names)}"
+ )
+ for slide_name, rels_file in references:
+ errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}")
+
+ if errors:
+ print(
+ f"FAILED - Found {len([e for e in errors if not e.startswith('' '')])} notes slide reference validation errors:"
+ )
+ for error in errors:
+ print(error)
+ print("Each slide may optionally have its own slide file.")
+ return False
+ else:
+ if self.verbose:
+ print("PASSED - All notes slide references are unique")
+ return True
+
+
+if __name__ == "__main__":
+ raise RuntimeError("This module should not be run directly.")
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'ooxml/scripts/validation/redlining.py', E'"""
+Validator for tracked changes in Word documents.
+"""
+
+import subprocess
+import tempfile
+import zipfile
+from pathlib import Path
+
+
+class RedliningValidator:
+ """Validator for tracked changes in Word documents."""
+
+ def __init__(self, unpacked_dir, original_docx, verbose=False):
+ self.unpacked_dir = Path(unpacked_dir)
+ self.original_docx = Path(original_docx)
+ self.verbose = verbose
+ self.namespaces = {
+ "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
+ }
+
+ def validate(self):
+ """Main validation method that returns True if valid, False otherwise."""
+ # Verify unpacked directory exists and has correct structure
+ modified_file = self.unpacked_dir / "word" / "document.xml"
+ if not modified_file.exists():
+ print(f"FAILED - Modified document.xml not found at {modified_file}")
+ return False
+
+ # First, check if there are any tracked changes by Claude to validate
+ try:
+ import xml.etree.ElementTree as ET
+
+ tree = ET.parse(modified_file)
+ root = tree.getroot()
+
+ # Check for w:del or w:ins tags authored by Claude
+ del_elements = root.findall(".//w:del", self.namespaces)
+ ins_elements = root.findall(".//w:ins", self.namespaces)
+
+ # Filter to only include changes by Claude
+ claude_del_elements = [
+ elem
+ for elem in del_elements
+ if elem.get(f"{{{self.namespaces[''w'']}}}author") == "Claude"
+ ]
+ claude_ins_elements = [
+ elem
+ for elem in ins_elements
+ if elem.get(f"{{{self.namespaces[''w'']}}}author") == "Claude"
+ ]
+
+ # Redlining validation is only needed if tracked changes by Claude have been used.
+ if not claude_del_elements and not claude_ins_elements:
+ if self.verbose:
+ print("PASSED - No tracked changes by Claude found.")
+ return True
+
+ except Exception:
+ # If we can''t parse the XML, continue with full validation
+ pass
+
+ # Create temporary directory for unpacking original docx
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_path = Path(temp_dir)
+
+ # Unpack original docx
+ try:
+ with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
+ zip_ref.extractall(temp_path)
+ except Exception as e:
+ print(f"FAILED - Error unpacking original docx: {e}")
+ return False
+
+ original_file = temp_path / "word" / "document.xml"
+ if not original_file.exists():
+ print(
+ f"FAILED - Original document.xml not found in {self.original_docx}"
+ )
+ return False
+
+ # Parse both XML files using xml.etree.ElementTree for redlining validation
+ try:
+ import xml.etree.ElementTree as ET
+
+ modified_tree = ET.parse(modified_file)
+ modified_root = modified_tree.getroot()
+ original_tree = ET.parse(original_file)
+ original_root = original_tree.getroot()
+ except ET.ParseError as e:
+ print(f"FAILED - Error parsing XML files: {e}")
+ return False
+
+ # Remove Claude''s tracked changes from both documents
+ self._remove_claude_tracked_changes(original_root)
+ self._remove_claude_tracked_changes(modified_root)
+
+ # Extract and compare text content
+ modified_text = self._extract_text_content(modified_root)
+ original_text = self._extract_text_content(original_root)
+
+ if modified_text != original_text:
+ # Show detailed character-level differences for each paragraph
+ error_message = self._generate_detailed_diff(
+ original_text, modified_text
+ )
+ print(error_message)
+ return False
+
+ if self.verbose:
+ print("PASSED - All changes by Claude are properly tracked")
+ return True
+
+ def _generate_detailed_diff(self, original_text, modified_text):
+ """Generate detailed word-level differences using git word diff."""
+ error_parts = [
+ "FAILED - Document text doesn''t match after removing Claude''s tracked changes",
+ "",
+ "Likely causes:",
+ " 1. Modified text inside another author''s or tags",
+ " 2. Made edits without proper tracked changes",
+ " 3. Didn''t nest inside when deleting another''s insertion",
+ "",
+ "For pre-redlined documents, use correct patterns:",
+ " - To reject another''s INSERTION: Nest inside their ",
+ " - To restore another''s DELETION: Add new AFTER their ",
+ "",
+ ]
+
+ # Show git word diff
+ git_diff = self._get_git_word_diff(original_text, modified_text)
+ if git_diff:
+ error_parts.extend(["Differences:", "============", git_diff])
+ else:
+ error_parts.append("Unable to generate word diff (git not available)")
+
+ return "\\n".join(error_parts)
+
+ def _get_git_word_diff(self, original_text, modified_text):
+ """Generate word diff using git with character-level precision."""
+ try:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_path = Path(temp_dir)
+
+ # Create two files
+ original_file = temp_path / "original.txt"
+ modified_file = temp_path / "modified.txt"
+
+ original_file.write_text(original_text, encoding="utf-8")
+ modified_file.write_text(modified_text, encoding="utf-8")
+
+ # Try character-level diff first for precise differences
+ result = subprocess.run(
+ [
+ "git",
+ "diff",
+ "--word-diff=plain",
+ "--word-diff-regex=.", # Character-by-character diff
+ "-U0", # Zero lines of context - show only changed lines
+ "--no-index",
+ str(original_file),
+ str(modified_file),
+ ],
+ capture_output=True,
+ text=True,
+ )
+
+ if result.stdout.strip():
+ # Clean up the output - remove git diff header lines
+ lines = result.stdout.split("\\n")
+ # Skip the header lines (diff --git, index, +++, ---, @@)
+ content_lines = []
+ in_content = False
+ for line in lines:
+ if line.startswith("@@"):
+ in_content = True
+ continue
+ if in_content and line.strip():
+ content_lines.append(line)
+
+ if content_lines:
+ return "\\n".join(content_lines)
+
+ # Fallback to word-level diff if character-level is too verbose
+ result = subprocess.run(
+ [
+ "git",
+ "diff",
+ "--word-diff=plain",
+ "-U0", # Zero lines of context
+ "--no-index",
+ str(original_file),
+ str(modified_file),
+ ],
+ capture_output=True,
+ text=True,
+ )
+
+ if result.stdout.strip():
+ lines = result.stdout.split("\\n")
+ content_lines = []
+ in_content = False
+ for line in lines:
+ if line.startswith("@@"):
+ in_content = True
+ continue
+ if in_content and line.strip():
+ content_lines.append(line)
+ return "\\n".join(content_lines)
+
+ except (subprocess.CalledProcessError, FileNotFoundError, Exception):
+ # Git not available or other error, return None to use fallback
+ pass
+
+ return None
+
+ def _remove_claude_tracked_changes(self, root):
+ """Remove tracked changes authored by Claude from the XML root."""
+ ins_tag = f"{{{self.namespaces[''w'']}}}ins"
+ del_tag = f"{{{self.namespaces[''w'']}}}del"
+ author_attr = f"{{{self.namespaces[''w'']}}}author"
+
+ # Remove w:ins elements
+ for parent in root.iter():
+ to_remove = []
+ for child in parent:
+ if child.tag == ins_tag and child.get(author_attr) == "Claude":
+ to_remove.append(child)
+ for elem in to_remove:
+ parent.remove(elem)
+
+ # Unwrap content in w:del elements where author is "Claude"
+ deltext_tag = f"{{{self.namespaces[''w'']}}}delText"
+ t_tag = f"{{{self.namespaces[''w'']}}}t"
+
+ for parent in root.iter():
+ to_process = []
+ for child in parent:
+ if child.tag == del_tag and child.get(author_attr) == "Claude":
+ to_process.append((child, list(parent).index(child)))
+
+ # Process in reverse order to maintain indices
+ for del_elem, del_index in reversed(to_process):
+ # Convert w:delText to w:t before moving
+ for elem in del_elem.iter():
+ if elem.tag == deltext_tag:
+ elem.tag = t_tag
+
+ # Move all children of w:del to its parent before removing w:del
+ for child in reversed(list(del_elem)):
+ parent.insert(del_index, child)
+ parent.remove(del_elem)
+
+ def _extract_text_content(self, root):
+ """Extract text content from Word XML, preserving paragraph structure.
+
+ Empty paragraphs are skipped to avoid false positives when tracked
+ insertions add only structural elements without text content.
+ """
+ p_tag = f"{{{self.namespaces[''w'']}}}p"
+ t_tag = f"{{{self.namespaces[''w'']}}}t"
+
+ paragraphs = []
+ for p_elem in root.findall(f".//{p_tag}"):
+ # Get all text elements within this paragraph
+ text_parts = []
+ for t_elem in p_elem.findall(f".//{t_tag}"):
+ if t_elem.text:
+ text_parts.append(t_elem.text)
+ paragraph_text = "".join(text_parts)
+ # Skip empty paragraphs - they don''t affect content validation
+ if paragraph_text:
+ paragraphs.append(paragraph_text)
+
+ return "\\n".join(paragraphs)
+
+
+if __name__ == "__main__":
+ raise RuntimeError("This module should not be run directly.")
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'scripts/html2pptx.js', E'/**
+ * html2pptx - Convert HTML slide to pptxgenjs slide with positioned elements
+ *
+ * USAGE:
+ * const pptx = new pptxgen();
+ * pptx.layout = ''LAYOUT_16x9''; // Must match HTML body dimensions
+ *
+ * const { slide, placeholders } = await html2pptx(''slide.html'', pptx);
+ * slide.addChart(pptx.charts.LINE, data, placeholders[0]);
+ *
+ * await pptx.writeFile(''output.pptx'');
+ *
+ * FEATURES:
+ * - Converts HTML to PowerPoint with accurate positioning
+ * - Supports text, images, shapes, and bullet lists
+ * - Extracts placeholder elements (class="placeholder") with positions
+ * - Handles CSS gradients, borders, and margins
+ *
+ * VALIDATION:
+ * - Uses body width/height from HTML for viewport sizing
+ * - Throws error if HTML dimensions don''t match presentation layout
+ * - Throws error if content overflows body (with overflow details)
+ *
+ * RETURNS:
+ * { slide, placeholders } where placeholders is an array of { id, x, y, w, h }
+ */
+
+const { chromium } = require(''playwright'');
+const path = require(''path'');
+const sharp = require(''sharp'');
+
+const PT_PER_PX = 0.75;
+const PX_PER_IN = 96;
+const EMU_PER_IN = 914400;
+
+// Helper: Get body dimensions and check for overflow
+async function getBodyDimensions(page) {
+ const bodyDimensions = await page.evaluate(() => {
+ const body = document.body;
+ const style = window.getComputedStyle(body);
+
+ return {
+ width: parseFloat(style.width),
+ height: parseFloat(style.height),
+ scrollWidth: body.scrollWidth,
+ scrollHeight: body.scrollHeight
+ };
+ });
+
+ const errors = [];
+ const widthOverflowPx = Math.max(0, bodyDimensions.scrollWidth - bodyDimensions.width - 1);
+ const heightOverflowPx = Math.max(0, bodyDimensions.scrollHeight - bodyDimensions.height - 1);
+
+ const widthOverflowPt = widthOverflowPx * PT_PER_PX;
+ const heightOverflowPt = heightOverflowPx * PT_PER_PX;
+
+ if (widthOverflowPt > 0 || heightOverflowPt > 0) {
+ const directions = [];
+ if (widthOverflowPt > 0) directions.push(`${widthOverflowPt.toFixed(1)}pt horizontally`);
+ if (heightOverflowPt > 0) directions.push(`${heightOverflowPt.toFixed(1)}pt vertically`);
+ const reminder = heightOverflowPt > 0 ? '' (Remember: leave 0.5" margin at bottom of slide)'' : '''';
+ errors.push(`HTML content overflows body by ${directions.join('' and '')}${reminder}`);
+ }
+
+ return { ...bodyDimensions, errors };
+}
+
+// Helper: Validate dimensions match presentation layout
+function validateDimensions(bodyDimensions, pres) {
+ const errors = [];
+ const widthInches = bodyDimensions.width / PX_PER_IN;
+ const heightInches = bodyDimensions.height / PX_PER_IN;
+
+ if (pres.presLayout) {
+ const layoutWidth = pres.presLayout.width / EMU_PER_IN;
+ const layoutHeight = pres.presLayout.height / EMU_PER_IN;
+
+ if (Math.abs(layoutWidth - widthInches) > 0.1 || Math.abs(layoutHeight - heightInches) > 0.1) {
+ errors.push(
+ `HTML dimensions (${widthInches.toFixed(1)}" × ${heightInches.toFixed(1)}") ` +
+ `don''t match presentation layout (${layoutWidth.toFixed(1)}" × ${layoutHeight.toFixed(1)}")`
+ );
+ }
+ }
+ return errors;
+}
+
+function validateTextBoxPosition(slideData, bodyDimensions) {
+ const errors = [];
+ const slideHeightInches = bodyDimensions.height / PX_PER_IN;
+ const minBottomMargin = 0.5; // 0.5 inches from bottom
+
+ for (const el of slideData.elements) {
+ // Check text elements (p, h1-h6, list)
+ if ([''p'', ''h1'', ''h2'', ''h3'', ''h4'', ''h5'', ''h6'', ''list''].includes(el.type)) {
+ const fontSize = el.style?.fontSize || 0;
+ const bottomEdge = el.position.y + el.position.h;
+ const distanceFromBottom = slideHeightInches - bottomEdge;
+
+ if (fontSize > 12 && distanceFromBottom < minBottomMargin) {
+ const getText = () => {
+ if (typeof el.text === ''string'') return el.text;
+ if (Array.isArray(el.text)) return el.text.find(t => t.text)?.text || '''';
+ if (Array.isArray(el.items)) return el.items.find(item => item.text)?.text || '''';
+ return '''';
+ };
+ const textPrefix = getText().substring(0, 50) + (getText().length > 50 ? ''...'' : '''');
+
+ errors.push(
+ `Text box "${textPrefix}" ends too close to bottom edge ` +
+ `(${distanceFromBottom.toFixed(2)}" from bottom, minimum ${minBottomMargin}" required)`
+ );
+ }
+ }
+ }
+
+ return errors;
+}
+
+// Helper: Add background to slide
+async function addBackground(slideData, targetSlide, tmpDir) {
+ if (slideData.background.type === ''image'' && slideData.background.path) {
+ let imagePath = slideData.background.path.startsWith(''file://'')
+ ? slideData.background.path.replace(''file://'', '''')
+ : slideData.background.path;
+ targetSlide.background = { path: imagePath };
+ } else if (slideData.background.type === ''color'' && slideData.background.value) {
+ targetSlide.background = { color: slideData.background.value };
+ }
+}
+
+// Helper: Add elements to slide
+function addElements(slideData, targetSlide, pres) {
+ for (const el of slideData.elements) {
+ if (el.type === ''image'') {
+ let imagePath = el.src.startsWith(''file://'') ? el.src.replace(''file://'', '''') : el.src;
+ targetSlide.addImage({
+ path: imagePath,
+ x: el.position.x,
+ y: el.position.y,
+ w: el.position.w,
+ h: el.position.h
+ });
+ } else if (el.type === ''line'') {
+ targetSlide.addShape(pres.ShapeType.line, {
+ x: el.x1,
+ y: el.y1,
+ w: el.x2 - el.x1,
+ h: el.y2 - el.y1,
+ line: { color: el.color, width: el.width }
+ });
+ } else if (el.type === ''shape'') {
+ const shapeOptions = {
+ x: el.position.x,
+ y: el.position.y,
+ w: el.position.w,
+ h: el.position.h,
+ shape: el.shape.rectRadius > 0 ? pres.ShapeType.roundRect : pres.ShapeType.rect
+ };
+
+ if (el.shape.fill) {
+ shapeOptions.fill = { color: el.shape.fill };
+ if (el.shape.transparency != null) shapeOptions.fill.transparency = el.shape.transparency;
+ }
+ if (el.shape.line) shapeOptions.line = el.shape.line;
+ if (el.shape.rectRadius > 0) shapeOptions.rectRadius = el.shape.rectRadius;
+ if (el.shape.shadow) shapeOptions.shadow = el.shape.shadow;
+
+ targetSlide.addText(el.text || '''', shapeOptions);
+ } else if (el.type === ''list'') {
+ const listOptions = {
+ x: el.position.x,
+ y: el.position.y,
+ w: el.position.w,
+ h: el.position.h,
+ fontSize: el.style.fontSize,
+ fontFace: el.style.fontFace,
+ color: el.style.color,
+ align: el.style.align,
+ valign: ''top'',
+ lineSpacing: el.style.lineSpacing,
+ paraSpaceBefore: el.style.paraSpaceBefore,
+ paraSpaceAfter: el.style.paraSpaceAfter,
+ margin: el.style.margin
+ };
+ if (el.style.margin) listOptions.margin = el.style.margin;
+ targetSlide.addText(el.items, listOptions);
+ } else {
+ // Check if text is single-line (height suggests one line)
+ const lineHeight = el.style.lineSpacing || el.style.fontSize * 1.2;
+ const isSingleLine = el.position.h <= lineHeight * 1.5;
+
+ let adjustedX = el.position.x;
+ let adjustedW = el.position.w;
+
+ // Make single-line text 2% wider to account for underestimate
+ if (isSingleLine) {
+ const widthIncrease = el.position.w * 0.02;
+ const align = el.style.align;
+
+ if (align === ''center'') {
+ // Center: expand both sides
+ adjustedX = el.position.x - (widthIncrease / 2);
+ adjustedW = el.position.w + widthIncrease;
+ } else if (align === ''right'') {
+ // Right: expand to the left
+ adjustedX = el.position.x - widthIncrease;
+ adjustedW = el.position.w + widthIncrease;
+ } else {
+ // Left (default): expand to the right
+ adjustedW = el.position.w + widthIncrease;
+ }
+ }
+
+ const textOptions = {
+ x: adjustedX,
+ y: el.position.y,
+ w: adjustedW,
+ h: el.position.h,
+ fontSize: el.style.fontSize,
+ fontFace: el.style.fontFace,
+ color: el.style.color,
+ bold: el.style.bold,
+ italic: el.style.italic,
+ underline: el.style.underline,
+ valign: ''top'',
+ lineSpacing: el.style.lineSpacing,
+ paraSpaceBefore: el.style.paraSpaceBefore,
+ paraSpaceAfter: el.style.paraSpaceAfter,
+ inset: 0 // Remove default PowerPoint internal padding
+ };
+
+ if (el.style.align) textOptions.align = el.style.align;
+ if (el.style.margin) textOptions.margin = el.style.margin;
+ if (el.style.rotate !== undefined) textOptions.rotate = el.style.rotate;
+ if (el.style.transparency !== null && el.style.transparency !== undefined) textOptions.transparency = el.style.transparency;
+
+ targetSlide.addText(el.text, textOptions);
+ }
+ }
+}
+
+// Helper: Extract slide data from HTML page
+async function extractSlideData(page) {
+ return await page.evaluate(() => {
+ const PT_PER_PX = 0.75;
+ const PX_PER_IN = 96;
+
+ // Fonts that are single-weight and should not have bold applied
+ // (applying bold causes PowerPoint to use faux bold which makes text wider)
+ const SINGLE_WEIGHT_FONTS = [''impact''];
+
+ // Helper: Check if a font should skip bold formatting
+ const shouldSkipBold = (fontFamily) => {
+ if (!fontFamily) return false;
+ const normalizedFont = fontFamily.toLowerCase().replace(/[''"]/g, '''').split('','')[0].trim();
+ return SINGLE_WEIGHT_FONTS.includes(normalizedFont);
+ };
+
+ // Unit conversion helpers
+ const pxToInch = (px) => px / PX_PER_IN;
+ const pxToPoints = (pxStr) => parseFloat(pxStr) * PT_PER_PX;
+ const rgbToHex = (rgbStr) => {
+ // Handle transparent backgrounds by defaulting to white
+ if (rgbStr === ''rgba(0, 0, 0, 0)'' || rgbStr === ''transparent'') return ''FFFFFF'';
+
+ const match = rgbStr.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);
+ if (!match) return ''FFFFFF'';
+ return match.slice(1).map(n => parseInt(n).toString(16).padStart(2, ''0'')).join('''');
+ };
+
+ const extractAlpha = (rgbStr) => {
+ const match = rgbStr.match(/rgba\\((\\d+),\\s*(\\d+),\\s*(\\d+),\\s*([\\d.]+)\\)/);
+ if (!match || !match[4]) return null;
+ const alpha = parseFloat(match[4]);
+ return Math.round((1 - alpha) * 100);
+ };
+
+ const applyTextTransform = (text, textTransform) => {
+ if (textTransform === ''uppercase'') return text.toUpperCase();
+ if (textTransform === ''lowercase'') return text.toLowerCase();
+ if (textTransform === ''capitalize'') {
+ return text.replace(/\\b\\w/g, c => c.toUpperCase());
+ }
+ return text;
+ };
+
+ // Extract rotation angle from CSS transform and writing-mode
+ const getRotation = (transform, writingMode) => {
+ let angle = 0;
+
+ // Handle writing-mode first
+ // PowerPoint: 90° = text rotated 90° clockwise (reads top to bottom, letters upright)
+ // PowerPoint: 270° = text rotated 270° clockwise (reads bottom to top, letters upright)
+ if (writingMode === ''vertical-rl'') {
+ // vertical-rl alone = text reads top to bottom = 90° in PowerPoint
+ angle = 90;
+ } else if (writingMode === ''vertical-lr'') {
+ // vertical-lr alone = text reads bottom to top = 270° in PowerPoint
+ angle = 270;
+ }
+
+ // Then add any transform rotation
+ if (transform && transform !== ''none'') {
+ // Try to match rotate() function
+ const rotateMatch = transform.match(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/);
+ if (rotateMatch) {
+ angle += parseFloat(rotateMatch[1]);
+ } else {
+ // Browser may compute as matrix - extract rotation from matrix
+ const matrixMatch = transform.match(/matrix\\(([^)]+)\\)/);
+ if (matrixMatch) {
+ const values = matrixMatch[1].split('','').map(parseFloat);
+ // matrix(a, b, c, d, e, f) where rotation = atan2(b, a)
+ const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI);
+ angle += Math.round(matrixAngle);
+ }
+ }
+ }
+
+ // Normalize to 0-359 range
+ angle = angle % 360;
+ if (angle < 0) angle += 360;
+
+ return angle === 0 ? null : angle;
+ };
+
+ // Get position/dimensions accounting for rotation
+ const getPositionAndSize = (el, rect, rotation) => {
+ if (rotation === null) {
+ return { x: rect.left, y: rect.top, w: rect.width, h: rect.height };
+ }
+
+ // For 90° or 270° rotations, swap width and height
+ // because PowerPoint applies rotation to the original (unrotated) box
+ const isVertical = rotation === 90 || rotation === 270;
+
+ if (isVertical) {
+ // The browser shows us the rotated dimensions (tall box for vertical text)
+ // But PowerPoint needs the pre-rotation dimensions (wide box that will be rotated)
+ // So we swap: browser''s height becomes PPT''s width, browser''s width becomes PPT''s height
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+
+ return {
+ x: centerX - rect.height / 2,
+ y: centerY - rect.width / 2,
+ w: rect.height,
+ h: rect.width
+ };
+ }
+
+ // For other rotations, use element''s offset dimensions
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+ return {
+ x: centerX - el.offsetWidth / 2,
+ y: centerY - el.offsetHeight / 2,
+ w: el.offsetWidth,
+ h: el.offsetHeight
+ };
+ };
+
+ // Parse CSS box-shadow into PptxGenJS shadow properties
+ const parseBoxShadow = (boxShadow) => {
+ if (!boxShadow || boxShadow === ''none'') return null;
+
+ // Browser computed style format: "rgba(0, 0, 0, 0.3) 2px 2px 8px 0px [inset]"
+ // CSS format: "[inset] 2px 2px 8px 0px rgba(0, 0, 0, 0.3)"
+
+ const insetMatch = boxShadow.match(/inset/);
+
+ // IMPORTANT: PptxGenJS/PowerPoint doesn''t properly support inset shadows
+ // Only process outer shadows to avoid file corruption
+ if (insetMatch) return null;
+
+ // Extract color first (rgba or rgb at start)
+ const colorMatch = boxShadow.match(/rgba?\\([^)]+\\)/);
+
+ // Extract numeric values (handles both px and pt units)
+ const parts = boxShadow.match(/([-\\d.]+)(px|pt)/g);
+
+ if (!parts || parts.length < 2) return null;
+
+ const offsetX = parseFloat(parts[0]);
+ const offsetY = parseFloat(parts[1]);
+ const blur = parts.length > 2 ? parseFloat(parts[2]) : 0;
+
+ // Calculate angle from offsets (in degrees, 0 = right, 90 = down)
+ let angle = 0;
+ if (offsetX !== 0 || offsetY !== 0) {
+ angle = Math.atan2(offsetY, offsetX) * (180 / Math.PI);
+ if (angle < 0) angle += 360;
+ }
+
+ // Calculate offset distance (hypotenuse)
+ const offset = Math.sqrt(offsetX * offsetX + offsetY * offsetY) * PT_PER_PX;
+
+ // Extract opacity from rgba
+ let opacity = 0.5;
+ if (colorMatch) {
+ const opacityMatch = colorMatch[0].match(/[\\d.]+\\)$/);
+ if (opacityMatch) {
+ opacity = parseFloat(opacityMatch[0].replace('')'', ''''));
+ }
+ }
+
+ return {
+ type: ''outer'',
+ angle: Math.round(angle),
+ blur: blur * 0.75, // Convert to points
+ color: colorMatch ? rgbToHex(colorMatch[0]) : ''000000'',
+ offset: offset,
+ opacity
+ };
+ };
+
+ // Parse inline formatting tags (, , , , , ) into text runs
+ const parseInlineFormatting = (element, baseOptions = {}, runs = [], baseTextTransform = (x) => x) => {
+ let prevNodeIsText = false;
+
+ element.childNodes.forEach((node) => {
+ let textTransform = baseTextTransform;
+
+ const isText = node.nodeType === Node.TEXT_NODE || node.tagName === ''BR'';
+ if (isText) {
+ const text = node.tagName === ''BR'' ? ''\\n'' : textTransform(node.textContent.replace(/\\s+/g, '' ''));
+ const prevRun = runs[runs.length - 1];
+ if (prevNodeIsText && prevRun) {
+ prevRun.text += text;
+ } else {
+ runs.push({ text, options: { ...baseOptions } });
+ }
+
+ } else if (node.nodeType === Node.ELEMENT_NODE && node.textContent.trim()) {
+ const options = { ...baseOptions };
+ const computed = window.getComputedStyle(node);
+
+ // Handle inline elements with computed styles
+ if (node.tagName === ''SPAN'' || node.tagName === ''B'' || node.tagName === ''STRONG'' || node.tagName === ''I'' || node.tagName === ''EM'' || node.tagName === ''U'') {
+ const isBold = computed.fontWeight === ''bold'' || parseInt(computed.fontWeight) >= 600;
+ if (isBold && !shouldSkipBold(computed.fontFamily)) options.bold = true;
+ if (computed.fontStyle === ''italic'') options.italic = true;
+ if (computed.textDecoration && computed.textDecoration.includes(''underline'')) options.underline = true;
+ if (computed.color && computed.color !== ''rgb(0, 0, 0)'') {
+ options.color = rgbToHex(computed.color);
+ const transparency = extractAlpha(computed.color);
+ if (transparency !== null) options.transparency = transparency;
+ }
+ if (computed.fontSize) options.fontSize = pxToPoints(computed.fontSize);
+
+ // Apply text-transform on the span element itself
+ if (computed.textTransform && computed.textTransform !== ''none'') {
+ const transformStr = computed.textTransform;
+ textTransform = (text) => applyTextTransform(text, transformStr);
+ }
+
+ // Validate: Check for margins on inline elements
+ if (computed.marginLeft && parseFloat(computed.marginLeft) > 0) {
+ errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`);
+ }
+ if (computed.marginRight && parseFloat(computed.marginRight) > 0) {
+ errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`);
+ }
+ if (computed.marginTop && parseFloat(computed.marginTop) > 0) {
+ errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`);
+ }
+ if (computed.marginBottom && parseFloat(computed.marginBottom) > 0) {
+ errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`);
+ }
+
+ // Recursively process the child node. This will flatten nested spans into multiple runs.
+ parseInlineFormatting(node, options, runs, textTransform);
+ }
+ }
+
+ prevNodeIsText = isText;
+ });
+
+ // Trim leading space from first run and trailing space from last run
+ if (runs.length > 0) {
+ runs[0].text = runs[0].text.replace(/^\\s+/, '''');
+ runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\\s+$/, '''');
+ }
+
+ return runs.filter(r => r.text.length > 0);
+ };
+
+ // Extract background from body (image or color)
+ const body = document.body;
+ const bodyStyle = window.getComputedStyle(body);
+ const bgImage = bodyStyle.backgroundImage;
+ const bgColor = bodyStyle.backgroundColor;
+
+ // Collect validation errors
+ const errors = [];
+
+ // Validate: Check for CSS gradients
+ if (bgImage && (bgImage.includes(''linear-gradient'') || bgImage.includes(''radial-gradient''))) {
+ errors.push(
+ ''CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, '' +
+ ''then reference with background-image: url(\\''gradient.png\\'')''
+ );
+ }
+
+ let background;
+ if (bgImage && bgImage !== ''none'') {
+ // Extract URL from url("...") or url(...)
+ const urlMatch = bgImage.match(/url\\(["'']?([^"'')]+)["'']?\\)/);
+ if (urlMatch) {
+ background = {
+ type: ''image'',
+ path: urlMatch[1]
+ };
+ } else {
+ background = {
+ type: ''color'',
+ value: rgbToHex(bgColor)
+ };
+ }
+ } else {
+ background = {
+ type: ''color'',
+ value: rgbToHex(bgColor)
+ };
+ }
+
+ // Process all elements
+ const elements = [];
+ const placeholders = [];
+ const textTags = [''P'', ''H1'', ''H2'', ''H3'', ''H4'', ''H5'', ''H6'', ''UL'', ''OL'', ''LI''];
+ const processed = new Set();
+
+ document.querySelectorAll(''*'').forEach((el) => {
+ if (processed.has(el)) return;
+
+ // Validate text elements don''t have backgrounds, borders, or shadows
+ if (textTags.includes(el.tagName)) {
+ const computed = window.getComputedStyle(el);
+ const hasBg = computed.backgroundColor && computed.backgroundColor !== ''rgba(0, 0, 0, 0)'';
+ const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) ||
+ (computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) ||
+ (computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) ||
+ (computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) ||
+ (computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0);
+ const hasShadow = computed.boxShadow && computed.boxShadow !== ''none'';
+
+ if (hasBg || hasBorder || hasShadow) {
+ errors.push(
+ `Text element <${el.tagName.toLowerCase()}> has ${hasBg ? ''background'' : hasBorder ? ''border'' : ''shadow''}. ` +
+ ''Backgrounds, borders, and shadows are only supported on elements, not text elements.''
+ );
+ return;
+ }
+ }
+
+ // Extract placeholder elements (for charts, etc.)
+ if (el.className && el.className.includes(''placeholder'')) {
+ const rect = el.getBoundingClientRect();
+ if (rect.width === 0 || rect.height === 0) {
+ errors.push(
+ `Placeholder "${el.id || ''unnamed''}" has ${rect.width === 0 ? ''width: 0'' : ''height: 0''}. Check the layout CSS.`
+ );
+ } else {
+ placeholders.push({
+ id: el.id || `placeholder-${placeholders.length}`,
+ x: pxToInch(rect.left),
+ y: pxToInch(rect.top),
+ w: pxToInch(rect.width),
+ h: pxToInch(rect.height)
+ });
+ }
+ processed.add(el);
+ return;
+ }
+
+ // Extract images
+ if (el.tagName === ''IMG'') {
+ const rect = el.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) {
+ elements.push({
+ type: ''image'',
+ src: el.src,
+ position: {
+ x: pxToInch(rect.left),
+ y: pxToInch(rect.top),
+ w: pxToInch(rect.width),
+ h: pxToInch(rect.height)
+ }
+ });
+ processed.add(el);
+ return;
+ }
+ }
+
+ // Extract DIVs with backgrounds/borders as shapes
+ const isContainer = el.tagName === ''DIV'' && !textTags.includes(el.tagName);
+ if (isContainer) {
+ const computed = window.getComputedStyle(el);
+ const hasBg = computed.backgroundColor && computed.backgroundColor !== ''rgba(0, 0, 0, 0)'';
+
+ // Validate: Check for unwrapped text content in DIV
+ for (const node of el.childNodes) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ const text = node.textContent.trim();
+ if (text) {
+ errors.push(
+ `DIV element contains unwrapped text "${text.substring(0, 50)}${text.length > 50 ? ''...'' : ''''}". ` +
+ ''All text must be wrapped in
,
-, , or tags to appear in PowerPoint.''
+ );
+ }
+ }
+ }
+
+ // Check for background images on shapes
+ const bgImage = computed.backgroundImage;
+ if (bgImage && bgImage !== ''none'') {
+ errors.push(
+ ''Background images on DIV elements are not supported. '' +
+ ''Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images.''
+ );
+ return;
+ }
+
+ // Check for borders - both uniform and partial
+ const borderTop = computed.borderTopWidth;
+ const borderRight = computed.borderRightWidth;
+ const borderBottom = computed.borderBottomWidth;
+ const borderLeft = computed.borderLeftWidth;
+ const borders = [borderTop, borderRight, borderBottom, borderLeft].map(b => parseFloat(b) || 0);
+ const hasBorder = borders.some(b => b > 0);
+ const hasUniformBorder = hasBorder && borders.every(b => b === borders[0]);
+ const borderLines = [];
+
+ if (hasBorder && !hasUniformBorder) {
+ const rect = el.getBoundingClientRect();
+ const x = pxToInch(rect.left);
+ const y = pxToInch(rect.top);
+ const w = pxToInch(rect.width);
+ const h = pxToInch(rect.height);
+
+ // Collect lines to add after shape (inset by half the line width to center on edge)
+ if (parseFloat(borderTop) > 0) {
+ const widthPt = pxToPoints(borderTop);
+ const inset = (widthPt / 72) / 2; // Convert points to inches, then half
+ borderLines.push({
+ type: ''line'',
+ x1: x, y1: y + inset, x2: x + w, y2: y + inset,
+ width: widthPt,
+ color: rgbToHex(computed.borderTopColor)
+ });
+ }
+ if (parseFloat(borderRight) > 0) {
+ const widthPt = pxToPoints(borderRight);
+ const inset = (widthPt / 72) / 2;
+ borderLines.push({
+ type: ''line'',
+ x1: x + w - inset, y1: y, x2: x + w - inset, y2: y + h,
+ width: widthPt,
+ color: rgbToHex(computed.borderRightColor)
+ });
+ }
+ if (parseFloat(borderBottom) > 0) {
+ const widthPt = pxToPoints(borderBottom);
+ const inset = (widthPt / 72) / 2;
+ borderLines.push({
+ type: ''line'',
+ x1: x, y1: y + h - inset, x2: x + w, y2: y + h - inset,
+ width: widthPt,
+ color: rgbToHex(computed.borderBottomColor)
+ });
+ }
+ if (parseFloat(borderLeft) > 0) {
+ const widthPt = pxToPoints(borderLeft);
+ const inset = (widthPt / 72) / 2;
+ borderLines.push({
+ type: ''line'',
+ x1: x + inset, y1: y, x2: x + inset, y2: y + h,
+ width: widthPt,
+ color: rgbToHex(computed.borderLeftColor)
+ });
+ }
+ }
+
+ if (hasBg || hasBorder) {
+ const rect = el.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) {
+ const shadow = parseBoxShadow(computed.boxShadow);
+
+ // Only add shape if there''s background or uniform border
+ if (hasBg || hasUniformBorder) {
+ elements.push({
+ type: ''shape'',
+ text: '''', // Shape only - child text elements render on top
+ position: {
+ x: pxToInch(rect.left),
+ y: pxToInch(rect.top),
+ w: pxToInch(rect.width),
+ h: pxToInch(rect.height)
+ },
+ shape: {
+ fill: hasBg ? rgbToHex(computed.backgroundColor) : null,
+ transparency: hasBg ? extractAlpha(computed.backgroundColor) : null,
+ line: hasUniformBorder ? {
+ color: rgbToHex(computed.borderColor),
+ width: pxToPoints(computed.borderWidth)
+ } : null,
+ // Convert border-radius to rectRadius (in inches)
+ // % values: 50%+ = circle (1), <50% = percentage of min dimension
+ // pt values: divide by 72 (72pt = 1 inch)
+ // px values: divide by 96 (96px = 1 inch)
+ rectRadius: (() => {
+ const radius = computed.borderRadius;
+ const radiusValue = parseFloat(radius);
+ if (radiusValue === 0) return 0;
+
+ if (radius.includes(''%'')) {
+ if (radiusValue >= 50) return 1;
+ // Calculate percentage of smaller dimension
+ const minDim = Math.min(rect.width, rect.height);
+ return (radiusValue / 100) * pxToInch(minDim);
+ }
+
+ if (radius.includes(''pt'')) return radiusValue / 72;
+ return radiusValue / PX_PER_IN;
+ })(),
+ shadow: shadow
+ }
+ });
+ }
+
+ // Add partial border lines
+ elements.push(...borderLines);
+
+ processed.add(el);
+ return;
+ }
+ }
+ }
+
+ // Extract bullet lists as single text block
+ if (el.tagName === ''UL'' || el.tagName === ''OL'') {
+ const rect = el.getBoundingClientRect();
+ if (rect.width === 0 || rect.height === 0) return;
+
+ const liElements = Array.from(el.querySelectorAll(''li''));
+ const items = [];
+ const ulComputed = window.getComputedStyle(el);
+ const ulPaddingLeftPt = pxToPoints(ulComputed.paddingLeft);
+
+ // Split: margin-left for bullet position, indent for text position
+ // margin-left + indent = ul padding-left
+ const marginLeft = ulPaddingLeftPt * 0.5;
+ const textIndent = ulPaddingLeftPt * 0.5;
+
+ liElements.forEach((li, idx) => {
+ const isLast = idx === liElements.length - 1;
+ const runs = parseInlineFormatting(li, { breakLine: false });
+ // Clean manual bullets from first run
+ if (runs.length > 0) {
+ runs[0].text = runs[0].text.replace(/^[•\\-\\*▪▸]\\s*/, '''');
+ runs[0].options.bullet = { indent: textIndent };
+ }
+ // Set breakLine on last run
+ if (runs.length > 0 && !isLast) {
+ runs[runs.length - 1].options.breakLine = true;
+ }
+ items.push(...runs);
+ });
+
+ const computed = window.getComputedStyle(liElements[0] || el);
+
+ elements.push({
+ type: ''list'',
+ items: items,
+ position: {
+ x: pxToInch(rect.left),
+ y: pxToInch(rect.top),
+ w: pxToInch(rect.width),
+ h: pxToInch(rect.height)
+ },
+ style: {
+ fontSize: pxToPoints(computed.fontSize),
+ fontFace: computed.fontFamily.split('','')[0].replace(/[''"]/g, '''').trim(),
+ color: rgbToHex(computed.color),
+ transparency: extractAlpha(computed.color),
+ align: computed.textAlign === ''start'' ? ''left'' : computed.textAlign,
+ lineSpacing: computed.lineHeight && computed.lineHeight !== ''normal'' ? pxToPoints(computed.lineHeight) : null,
+ paraSpaceBefore: 0,
+ paraSpaceAfter: pxToPoints(computed.marginBottom),
+ // PptxGenJS margin array is [left, right, bottom, top]
+ margin: [marginLeft, 0, 0, 0]
+ }
+ });
+
+ liElements.forEach(li => processed.add(li));
+ processed.add(el);
+ return;
+ }
+
+ // Extract text elements (P, H1, H2, etc.)
+ if (!textTags.includes(el.tagName)) return;
+
+ const rect = el.getBoundingClientRect();
+ const text = el.textContent.trim();
+ if (rect.width === 0 || rect.height === 0 || !text) return;
+
+ // Validate: Check for manual bullet symbols in text elements (not in lists)
+ if (el.tagName !== ''LI'' && /^[•\\-\\*▪▸○●◆◇■□]\\s/.test(text.trimStart())) {
+ errors.push(
+ `Text element <${el.tagName.toLowerCase()}> starts with bullet symbol "${text.substring(0, 20)}...". ` +
+ ''Use or lists instead of manual bullet symbols.''
+ );
+ return;
+ }
+
+ const computed = window.getComputedStyle(el);
+ const rotation = getRotation(computed.transform, computed.writingMode);
+ const { x, y, w, h } = getPositionAndSize(el, rect, rotation);
+
+ const baseStyle = {
+ fontSize: pxToPoints(computed.fontSize),
+ fontFace: computed.fontFamily.split('','')[0].replace(/[''"]/g, '''').trim(),
+ color: rgbToHex(computed.color),
+ align: computed.textAlign === ''start'' ? ''left'' : computed.textAlign,
+ lineSpacing: pxToPoints(computed.lineHeight),
+ paraSpaceBefore: pxToPoints(computed.marginTop),
+ paraSpaceAfter: pxToPoints(computed.marginBottom),
+ // PptxGenJS margin array is [left, right, bottom, top] (not [top, right, bottom, left] as documented)
+ margin: [
+ pxToPoints(computed.paddingLeft),
+ pxToPoints(computed.paddingRight),
+ pxToPoints(computed.paddingBottom),
+ pxToPoints(computed.paddingTop)
+ ]
+ };
+
+ const transparency = extractAlpha(computed.color);
+ if (transparency !== null) baseStyle.transparency = transparency;
+
+ if (rotation !== null) baseStyle.rotate = rotation;
+
+ const hasFormatting = el.querySelector(''b, i, u, strong, em, span, br'');
+
+ if (hasFormatting) {
+ // Text with inline formatting
+ const transformStr = computed.textTransform;
+ const runs = parseInlineFormatting(el, {}, [], (str) => applyTextTransform(str, transformStr));
+
+ // Adjust lineSpacing based on largest fontSize in runs
+ const adjustedStyle = { ...baseStyle };
+ if (adjustedStyle.lineSpacing) {
+ const maxFontSize = Math.max(
+ adjustedStyle.fontSize,
+ ...runs.map(r => r.options?.fontSize || 0)
+ );
+ if (maxFontSize > adjustedStyle.fontSize) {
+ const lineHeightMultiplier = adjustedStyle.lineSpacing / adjustedStyle.fontSize;
+ adjustedStyle.lineSpacing = maxFontSize * lineHeightMultiplier;
+ }
+ }
+
+ elements.push({
+ type: el.tagName.toLowerCase(),
+ text: runs,
+ position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) },
+ style: adjustedStyle
+ });
+ } else {
+ // Plain text - inherit CSS formatting
+ const textTransform = computed.textTransform;
+ const transformedText = applyTextTransform(text, textTransform);
+
+ const isBold = computed.fontWeight === ''bold'' || parseInt(computed.fontWeight) >= 600;
+
+ elements.push({
+ type: el.tagName.toLowerCase(),
+ text: transformedText,
+ position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) },
+ style: {
+ ...baseStyle,
+ bold: isBold && !shouldSkipBold(computed.fontFamily),
+ italic: computed.fontStyle === ''italic'',
+ underline: computed.textDecoration.includes(''underline'')
+ }
+ });
+ }
+
+ processed.add(el);
+ });
+
+ return { background, elements, placeholders, errors };
+ });
+}
+
+async function html2pptx(htmlFile, pres, options = {}) {
+ const {
+ tmpDir = process.env.TMPDIR || ''/tmp'',
+ slide = null
+ } = options;
+
+ try {
+ // Use Chrome on macOS, default Chromium on Unix
+ const launchOptions = { env: { TMPDIR: tmpDir } };
+ if (process.platform === ''darwin'') {
+ launchOptions.channel = ''chrome'';
+ }
+
+ const browser = await chromium.launch(launchOptions);
+
+ let bodyDimensions;
+ let slideData;
+
+ const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
+ const validationErrors = [];
+
+ try {
+ const page = await browser.newPage();
+ page.on(''console'', (msg) => {
+ // Log the message text to your test runner''s console
+ console.log(`Browser console: ${msg.text()}`);
+ });
+
+ await page.goto(`file://${filePath}`);
+
+ bodyDimensions = await getBodyDimensions(page);
+
+ await page.setViewportSize({
+ width: Math.round(bodyDimensions.width),
+ height: Math.round(bodyDimensions.height)
+ });
+
+ slideData = await extractSlideData(page);
+ } finally {
+ await browser.close();
+ }
+
+ // Collect all validation errors
+ if (bodyDimensions.errors && bodyDimensions.errors.length > 0) {
+ validationErrors.push(...bodyDimensions.errors);
+ }
+
+ const dimensionErrors = validateDimensions(bodyDimensions, pres);
+ if (dimensionErrors.length > 0) {
+ validationErrors.push(...dimensionErrors);
+ }
+
+ const textBoxPositionErrors = validateTextBoxPosition(slideData, bodyDimensions);
+ if (textBoxPositionErrors.length > 0) {
+ validationErrors.push(...textBoxPositionErrors);
+ }
+
+ if (slideData.errors && slideData.errors.length > 0) {
+ validationErrors.push(...slideData.errors);
+ }
+
+ // Throw all errors at once if any exist
+ if (validationErrors.length > 0) {
+ const errorMessage = validationErrors.length === 1
+ ? validationErrors[0]
+ : `Multiple validation errors found:\\n${validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join(''\\n'')}`;
+ throw new Error(errorMessage);
+ }
+
+ const targetSlide = slide || pres.addSlide();
+
+ await addBackground(slideData, targetSlide, tmpDir);
+ addElements(slideData, targetSlide, pres);
+
+ return { slide: targetSlide, placeholders: slideData.placeholders };
+ } catch (error) {
+ if (!error.message.startsWith(htmlFile)) {
+ throw new Error(`${htmlFile}: ${error.message}`);
+ }
+ throw error;
+ }
+}
+
+module.exports = html2pptx;' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'scripts/inventory.py', E'#!/usr/bin/env python3
+"""
+Extract structured text content from PowerPoint presentations.
+
+This module provides functionality to:
+- Extract all text content from PowerPoint shapes
+- Preserve paragraph formatting (alignment, bullets, fonts, spacing)
+- Handle nested GroupShapes recursively with correct absolute positions
+- Sort shapes by visual position on slides
+- Filter out slide numbers and non-content placeholders
+- Export to JSON with clean, structured data
+
+Classes:
+ ParagraphData: Represents a text paragraph with formatting
+ ShapeData: Represents a shape with position and text content
+
+Main Functions:
+ extract_text_inventory: Extract all text from a presentation
+ save_inventory: Save extracted data to JSON
+
+Usage:
+ python inventory.py input.pptx output.json
+"""
+
+import argparse
+import json
+import platform
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple, Union
+
+from PIL import Image, ImageDraw, ImageFont
+from pptx import Presentation
+from pptx.enum.text import PP_ALIGN
+from pptx.shapes.base import BaseShape
+
+# Type aliases for cleaner signatures
+JsonValue = Union[str, int, float, bool, None]
+ParagraphDict = Dict[str, JsonValue]
+ShapeDict = Dict[
+ str, Union[str, float, bool, List[ParagraphDict], List[str], Dict[str, Any], None]
+]
+InventoryData = Dict[
+ str, Dict[str, "ShapeData"]
+] # Dict of slide_id -> {shape_id -> ShapeData}
+InventoryDict = Dict[str, Dict[str, ShapeDict]] # JSON-serializable inventory
+
+
+def main():
+ """Main entry point for command-line usage."""
+ parser = argparse.ArgumentParser(
+ description="Extract text inventory from PowerPoint with proper GroupShape support.",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ python inventory.py presentation.pptx inventory.json
+ Extracts text inventory with correct absolute positions for grouped shapes
+
+ python inventory.py presentation.pptx inventory.json --issues-only
+ Extracts only text shapes that have overflow or overlap issues
+
+The output JSON includes:
+ - All text content organized by slide and shape
+ - Correct absolute positions for shapes in groups
+ - Visual position and size in inches
+ - Paragraph properties and formatting
+ - Issue detection: text overflow and shape overlaps
+ """,
+ )
+
+ parser.add_argument("input", help="Input PowerPoint file (.pptx)")
+ parser.add_argument("output", help="Output JSON file for inventory")
+ parser.add_argument(
+ "--issues-only",
+ action="store_true",
+ help="Include only text shapes that have overflow or overlap issues",
+ )
+
+ args = parser.parse_args()
+
+ input_path = Path(args.input)
+ if not input_path.exists():
+ print(f"Error: Input file not found: {args.input}")
+ sys.exit(1)
+
+ if not input_path.suffix.lower() == ".pptx":
+ print("Error: Input must be a PowerPoint file (.pptx)")
+ sys.exit(1)
+
+ try:
+ print(f"Extracting text inventory from: {args.input}")
+ if args.issues_only:
+ print(
+ "Filtering to include only text shapes with issues (overflow/overlap)"
+ )
+ inventory = extract_text_inventory(input_path, issues_only=args.issues_only)
+
+ output_path = Path(args.output)
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ save_inventory(inventory, output_path)
+
+ print(f"Output saved to: {args.output}")
+
+ # Report statistics
+ total_slides = len(inventory)
+ total_shapes = sum(len(shapes) for shapes in inventory.values())
+ if args.issues_only:
+ if total_shapes > 0:
+ print(
+ f"Found {total_shapes} text elements with issues in {total_slides} slides"
+ )
+ else:
+ print("No issues discovered")
+ else:
+ print(
+ f"Found text in {total_slides} slides with {total_shapes} text elements"
+ )
+
+ except Exception as e:
+ print(f"Error processing presentation: {e}")
+ import traceback
+
+ traceback.print_exc()
+ sys.exit(1)
+
+
+@dataclass
+class ShapeWithPosition:
+ """A shape with its absolute position on the slide."""
+
+ shape: BaseShape
+ absolute_left: int # in EMUs
+ absolute_top: int # in EMUs
+
+
+class ParagraphData:
+ """Data structure for paragraph properties extracted from a PowerPoint paragraph."""
+
+ def __init__(self, paragraph: Any):
+ """Initialize from a PowerPoint paragraph object.
+
+ Args:
+ paragraph: The PowerPoint paragraph object
+ """
+ self.text: str = paragraph.text.strip()
+ self.bullet: bool = False
+ self.level: Optional[int] = None
+ self.alignment: Optional[str] = None
+ self.space_before: Optional[float] = None
+ self.space_after: Optional[float] = None
+ self.font_name: Optional[str] = None
+ self.font_size: Optional[float] = None
+ self.bold: Optional[bool] = None
+ self.italic: Optional[bool] = None
+ self.underline: Optional[bool] = None
+ self.color: Optional[str] = None
+ self.theme_color: Optional[str] = None
+ self.line_spacing: Optional[float] = None
+
+ # Check for bullet formatting
+ if (
+ hasattr(paragraph, "_p")
+ and paragraph._p is not None
+ and paragraph._p.pPr is not None
+ ):
+ pPr = paragraph._p.pPr
+ ns = "{http://schemas.openxmlformats.org/drawingml/2006/main}"
+ if (
+ pPr.find(f"{ns}buChar") is not None
+ or pPr.find(f"{ns}buAutoNum") is not None
+ ):
+ self.bullet = True
+ if hasattr(paragraph, "level"):
+ self.level = paragraph.level
+
+ # Add alignment if not LEFT (default)
+ if hasattr(paragraph, "alignment") and paragraph.alignment is not None:
+ alignment_map = {
+ PP_ALIGN.CENTER: "CENTER",
+ PP_ALIGN.RIGHT: "RIGHT",
+ PP_ALIGN.JUSTIFY: "JUSTIFY",
+ }
+ if paragraph.alignment in alignment_map:
+ self.alignment = alignment_map[paragraph.alignment]
+
+ # Add spacing properties if set
+ if hasattr(paragraph, "space_before") and paragraph.space_before:
+ self.space_before = paragraph.space_before.pt
+ if hasattr(paragraph, "space_after") and paragraph.space_after:
+ self.space_after = paragraph.space_after.pt
+
+ # Extract font properties from first run
+ if paragraph.runs:
+ first_run = paragraph.runs[0]
+ if hasattr(first_run, "font"):
+ font = first_run.font
+ if font.name:
+ self.font_name = font.name
+ if font.size:
+ self.font_size = font.size.pt
+ if font.bold is not None:
+ self.bold = font.bold
+ if font.italic is not None:
+ self.italic = font.italic
+ if font.underline is not None:
+ self.underline = font.underline
+
+ # Handle color - both RGB and theme colors
+ try:
+ # Try RGB color first
+ if font.color.rgb:
+ self.color = str(font.color.rgb)
+ except (AttributeError, TypeError):
+ # Fall back to theme color
+ try:
+ if font.color.theme_color:
+ self.theme_color = font.color.theme_color.name
+ except (AttributeError, TypeError):
+ pass
+
+ # Add line spacing if set
+ if hasattr(paragraph, "line_spacing") and paragraph.line_spacing is not None:
+ if hasattr(paragraph.line_spacing, "pt"):
+ self.line_spacing = round(paragraph.line_spacing.pt, 2)
+ else:
+ # Multiplier - convert to points
+ font_size = self.font_size if self.font_size else 12.0
+ self.line_spacing = round(paragraph.line_spacing * font_size, 2)
+
+ def to_dict(self) -> ParagraphDict:
+ """Convert to dictionary for JSON serialization, excluding None values."""
+ result: ParagraphDict = {"text": self.text}
+
+ # Add optional fields only if they have values
+ if self.bullet:
+ result["bullet"] = self.bullet
+ if self.level is not None:
+ result["level"] = self.level
+ if self.alignment:
+ result["alignment"] = self.alignment
+ if self.space_before is not None:
+ result["space_before"] = self.space_before
+ if self.space_after is not None:
+ result["space_after"] = self.space_after
+ if self.font_name:
+ result["font_name"] = self.font_name
+ if self.font_size is not None:
+ result["font_size"] = self.font_size
+ if self.bold is not None:
+ result["bold"] = self.bold
+ if self.italic is not None:
+ result["italic"] = self.italic
+ if self.underline is not None:
+ result["underline"] = self.underline
+ if self.color:
+ result["color"] = self.color
+ if self.theme_color:
+ result["theme_color"] = self.theme_color
+ if self.line_spacing is not None:
+ result["line_spacing"] = self.line_spacing
+
+ return result
+
+
+class ShapeData:
+ """Data structure for shape properties extracted from a PowerPoint shape."""
+
+ @staticmethod
+ def emu_to_inches(emu: int) -> float:
+ """Convert EMUs (English Metric Units) to inches."""
+ return emu / 914400.0
+
+ @staticmethod
+ def inches_to_pixels(inches: float, dpi: int = 96) -> int:
+ """Convert inches to pixels at given DPI."""
+ return int(inches * dpi)
+
+ @staticmethod
+ def get_font_path(font_name: str) -> Optional[str]:
+ """Get the font file path for a given font name.
+
+ Args:
+ font_name: Name of the font (e.g., ''Arial'', ''Calibri'')
+
+ Returns:
+ Path to the font file, or None if not found
+ """
+ system = platform.system()
+
+ # Common font file variations to try
+ font_variations = [
+ font_name,
+ font_name.lower(),
+ font_name.replace(" ", ""),
+ font_name.replace(" ", "-"),
+ ]
+
+ # Define font directories and extensions by platform
+ if system == "Darwin": # macOS
+ font_dirs = [
+ "/System/Library/Fonts/",
+ "/Library/Fonts/",
+ "~/Library/Fonts/",
+ ]
+ extensions = [".ttf", ".otf", ".ttc", ".dfont"]
+ else: # Linux
+ font_dirs = [
+ "/usr/share/fonts/truetype/",
+ "/usr/local/share/fonts/",
+ "~/.fonts/",
+ ]
+ extensions = [".ttf", ".otf"]
+
+ # Try to find the font file
+ from pathlib import Path
+
+ for font_dir in font_dirs:
+ font_dir_path = Path(font_dir).expanduser()
+ if not font_dir_path.exists():
+ continue
+
+ # First try exact matches
+ for variant in font_variations:
+ for ext in extensions:
+ font_path = font_dir_path / f"{variant}{ext}"
+ if font_path.exists():
+ return str(font_path)
+
+ # Then try fuzzy matching - find files containing the font name
+ try:
+ for file_path in font_dir_path.iterdir():
+ if file_path.is_file():
+ file_name_lower = file_path.name.lower()
+ font_name_lower = font_name.lower().replace(" ", "")
+ if font_name_lower in file_name_lower and any(
+ file_name_lower.endswith(ext) for ext in extensions
+ ):
+ return str(file_path)
+ except (OSError, PermissionError):
+ continue
+
+ return None
+
+ @staticmethod
+ def get_slide_dimensions(slide: Any) -> tuple[Optional[int], Optional[int]]:
+ """Get slide dimensions from slide object.
+
+ Args:
+ slide: Slide object
+
+ Returns:
+ Tuple of (width_emu, height_emu) or (None, None) if not found
+ """
+ try:
+ prs = slide.part.package.presentation_part.presentation
+ return prs.slide_width, prs.slide_height
+ except (AttributeError, TypeError):
+ return None, None
+
+ @staticmethod
+ def get_default_font_size(shape: BaseShape, slide_layout: Any) -> Optional[float]:
+ """Extract default font size from slide layout for a placeholder shape.
+
+ Args:
+ shape: Placeholder shape
+ slide_layout: Slide layout containing the placeholder definition
+
+ Returns:
+ Default font size in points, or None if not found
+ """
+ try:
+ if not hasattr(shape, "placeholder_format"):
+ return None
+
+ shape_type = shape.placeholder_format.type # type: ignore
+ for layout_placeholder in slide_layout.placeholders:
+ if layout_placeholder.placeholder_format.type == shape_type:
+ # Find first defRPr element with sz (size) attribute
+ for elem in layout_placeholder.element.iter():
+ if "defRPr" in elem.tag and (sz := elem.get("sz")):
+ return float(sz) / 100.0 # Convert EMUs to points
+ break
+ except Exception:
+ pass
+ return None
+
+ def __init__(
+ self,
+ shape: BaseShape,
+ absolute_left: Optional[int] = None,
+ absolute_top: Optional[int] = None,
+ slide: Optional[Any] = None,
+ ):
+ """Initialize from a PowerPoint shape object.
+
+ Args:
+ shape: The PowerPoint shape object (should be pre-validated)
+ absolute_left: Absolute left position in EMUs (for shapes in groups)
+ absolute_top: Absolute top position in EMUs (for shapes in groups)
+ slide: Optional slide object to get dimensions and layout information
+ """
+ self.shape = shape # Store reference to original shape
+ self.shape_id: str = "" # Will be set after sorting
+
+ # Get slide dimensions from slide object
+ self.slide_width_emu, self.slide_height_emu = (
+ self.get_slide_dimensions(slide) if slide else (None, None)
+ )
+
+ # Get placeholder type if applicable
+ self.placeholder_type: Optional[str] = None
+ self.default_font_size: Optional[float] = None
+ if hasattr(shape, "is_placeholder") and shape.is_placeholder: # type: ignore
+ if shape.placeholder_format and shape.placeholder_format.type: # type: ignore
+ self.placeholder_type = (
+ str(shape.placeholder_format.type).split(".")[-1].split(" ")[0] # type: ignore
+ )
+
+ # Get default font size from layout
+ if slide and hasattr(slide, "slide_layout"):
+ self.default_font_size = self.get_default_font_size(
+ shape, slide.slide_layout
+ )
+
+ # Get position information
+ # Use absolute positions if provided (for shapes in groups), otherwise use shape''s position
+ left_emu = (
+ absolute_left
+ if absolute_left is not None
+ else (shape.left if hasattr(shape, "left") else 0)
+ )
+ top_emu = (
+ absolute_top
+ if absolute_top is not None
+ else (shape.top if hasattr(shape, "top") else 0)
+ )
+
+ self.left: float = round(self.emu_to_inches(left_emu), 2) # type: ignore
+ self.top: float = round(self.emu_to_inches(top_emu), 2) # type: ignore
+ self.width: float = round(
+ self.emu_to_inches(shape.width if hasattr(shape, "width") else 0),
+ 2, # type: ignore
+ )
+ self.height: float = round(
+ self.emu_to_inches(shape.height if hasattr(shape, "height") else 0),
+ 2, # type: ignore
+ )
+
+ # Store EMU positions for overflow calculations
+ self.left_emu = left_emu
+ self.top_emu = top_emu
+ self.width_emu = shape.width if hasattr(shape, "width") else 0
+ self.height_emu = shape.height if hasattr(shape, "height") else 0
+
+ # Calculate overflow status
+ self.frame_overflow_bottom: Optional[float] = None
+ self.slide_overflow_right: Optional[float] = None
+ self.slide_overflow_bottom: Optional[float] = None
+ self.overlapping_shapes: Dict[
+ str, float
+ ] = {} # Dict of shape_id -> overlap area in sq inches
+ self.warnings: List[str] = []
+ self._estimate_frame_overflow()
+ self._calculate_slide_overflow()
+ self._detect_bullet_issues()
+
+ @property
+ def paragraphs(self) -> List[ParagraphData]:
+ """Calculate paragraphs from the shape''s text frame."""
+ if not self.shape or not hasattr(self.shape, "text_frame"):
+ return []
+
+ paragraphs = []
+ for paragraph in self.shape.text_frame.paragraphs: # type: ignore
+ if paragraph.text.strip():
+ paragraphs.append(ParagraphData(paragraph))
+ return paragraphs
+
+ def _get_default_font_size(self) -> int:
+ """Get default font size from theme text styles or use conservative default."""
+ try:
+ if not (
+ hasattr(self.shape, "part") and hasattr(self.shape.part, "slide_layout")
+ ):
+ return 14
+
+ slide_master = self.shape.part.slide_layout.slide_master # type: ignore
+ if not hasattr(slide_master, "element"):
+ return 14
+
+ # Determine theme style based on placeholder type
+ style_name = "bodyStyle" # Default
+ if self.placeholder_type and "TITLE" in self.placeholder_type:
+ style_name = "titleStyle"
+
+ # Find font size in theme styles
+ for child in slide_master.element.iter():
+ tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
+ if tag == style_name:
+ for elem in child.iter():
+ if "sz" in elem.attrib:
+ return int(elem.attrib["sz"]) // 100
+ except Exception:
+ pass
+
+ return 14 # Conservative default for body text
+
+ def _get_usable_dimensions(self, text_frame) -> Tuple[int, int]:
+ """Get usable width and height in pixels after accounting for margins."""
+ # Default PowerPoint margins in inches
+ margins = {"top": 0.05, "bottom": 0.05, "left": 0.1, "right": 0.1}
+
+ # Override with actual margins if set
+ if hasattr(text_frame, "margin_top") and text_frame.margin_top:
+ margins["top"] = self.emu_to_inches(text_frame.margin_top)
+ if hasattr(text_frame, "margin_bottom") and text_frame.margin_bottom:
+ margins["bottom"] = self.emu_to_inches(text_frame.margin_bottom)
+ if hasattr(text_frame, "margin_left") and text_frame.margin_left:
+ margins["left"] = self.emu_to_inches(text_frame.margin_left)
+ if hasattr(text_frame, "margin_right") and text_frame.margin_right:
+ margins["right"] = self.emu_to_inches(text_frame.margin_right)
+
+ # Calculate usable area
+ usable_width = self.width - margins["left"] - margins["right"]
+ usable_height = self.height - margins["top"] - margins["bottom"]
+
+ # Convert to pixels
+ return (
+ self.inches_to_pixels(usable_width),
+ self.inches_to_pixels(usable_height),
+ )
+
+ def _wrap_text_line(self, line: str, max_width_px: int, draw, font) -> List[str]:
+ """Wrap a single line of text to fit within max_width_px."""
+ if not line:
+ return [""]
+
+ # Use textlength for efficient width calculation
+ if draw.textlength(line, font=font) <= max_width_px:
+ return [line]
+
+ # Need to wrap - split into words
+ wrapped = []
+ words = line.split(" ")
+ current_line = ""
+
+ for word in words:
+ test_line = current_line + (" " if current_line else "") + word
+ if draw.textlength(test_line, font=font) <= max_width_px:
+ current_line = test_line
+ else:
+ if current_line:
+ wrapped.append(current_line)
+ current_line = word
+
+ if current_line:
+ wrapped.append(current_line)
+
+ return wrapped
+
+ def _estimate_frame_overflow(self) -> None:
+ """Estimate if text overflows the shape bounds using PIL text measurement."""
+ if not self.shape or not hasattr(self.shape, "text_frame"):
+ return
+
+ text_frame = self.shape.text_frame # type: ignore
+ if not text_frame or not text_frame.paragraphs:
+ return
+
+ # Get usable dimensions after accounting for margins
+ usable_width_px, usable_height_px = self._get_usable_dimensions(text_frame)
+ if usable_width_px <= 0 or usable_height_px <= 0:
+ return
+
+ # Set up PIL for text measurement
+ dummy_img = Image.new("RGB", (1, 1))
+ draw = ImageDraw.Draw(dummy_img)
+
+ # Get default font size from placeholder or use conservative estimate
+ default_font_size = self._get_default_font_size()
+
+ # Calculate total height of all paragraphs
+ total_height_px = 0
+
+ for para_idx, paragraph in enumerate(text_frame.paragraphs):
+ if not paragraph.text.strip():
+ continue
+
+ para_data = ParagraphData(paragraph)
+
+ # Load font for this paragraph
+ font_name = para_data.font_name or "Arial"
+ font_size = int(para_data.font_size or default_font_size)
+
+ font = None
+ font_path = self.get_font_path(font_name)
+ if font_path:
+ try:
+ font = ImageFont.truetype(font_path, size=font_size)
+ except Exception:
+ font = ImageFont.load_default()
+ else:
+ font = ImageFont.load_default()
+
+ # Wrap all lines in this paragraph
+ all_wrapped_lines = []
+ for line in paragraph.text.split("\\n"):
+ wrapped = self._wrap_text_line(line, usable_width_px, draw, font)
+ all_wrapped_lines.extend(wrapped)
+
+ if all_wrapped_lines:
+ # Calculate line height
+ if para_data.line_spacing:
+ # Custom line spacing explicitly set
+ line_height_px = para_data.line_spacing * 96 / 72
+ else:
+ # PowerPoint default single spacing (1.0x font size)
+ line_height_px = font_size * 96 / 72
+
+ # Add space_before (except first paragraph)
+ if para_idx > 0 and para_data.space_before:
+ total_height_px += para_data.space_before * 96 / 72
+
+ # Add paragraph text height
+ total_height_px += len(all_wrapped_lines) * line_height_px
+
+ # Add space_after
+ if para_data.space_after:
+ total_height_px += para_data.space_after * 96 / 72
+
+ # Check for overflow (ignore negligible overflows <= 0.05")
+ if total_height_px > usable_height_px:
+ overflow_px = total_height_px - usable_height_px
+ overflow_inches = round(overflow_px / 96.0, 2)
+ if overflow_inches > 0.05: # Only report significant overflows
+ self.frame_overflow_bottom = overflow_inches
+
+ def _calculate_slide_overflow(self) -> None:
+ """Calculate if shape overflows the slide boundaries."""
+ if self.slide_width_emu is None or self.slide_height_emu is None:
+ return
+
+ # Check right overflow (ignore negligible overflows <= 0.01")
+ right_edge_emu = self.left_emu + self.width_emu
+ if right_edge_emu > self.slide_width_emu:
+ overflow_emu = right_edge_emu - self.slide_width_emu
+ overflow_inches = round(self.emu_to_inches(overflow_emu), 2)
+ if overflow_inches > 0.01: # Only report significant overflows
+ self.slide_overflow_right = overflow_inches
+
+ # Check bottom overflow (ignore negligible overflows <= 0.01")
+ bottom_edge_emu = self.top_emu + self.height_emu
+ if bottom_edge_emu > self.slide_height_emu:
+ overflow_emu = bottom_edge_emu - self.slide_height_emu
+ overflow_inches = round(self.emu_to_inches(overflow_emu), 2)
+ if overflow_inches > 0.01: # Only report significant overflows
+ self.slide_overflow_bottom = overflow_inches
+
+ def _detect_bullet_issues(self) -> None:
+ """Detect bullet point formatting issues in paragraphs."""
+ if not self.shape or not hasattr(self.shape, "text_frame"):
+ return
+
+ text_frame = self.shape.text_frame # type: ignore
+ if not text_frame or not text_frame.paragraphs:
+ return
+
+ # Common bullet symbols that indicate manual bullets
+ bullet_symbols = ["•", "●", "○"]
+
+ for paragraph in text_frame.paragraphs:
+ text = paragraph.text.strip()
+ # Check for manual bullet symbols
+ if text and any(text.startswith(symbol + " ") for symbol in bullet_symbols):
+ self.warnings.append(
+ "manual_bullet_symbol: use proper bullet formatting"
+ )
+ break
+
+ @property
+ def has_any_issues(self) -> bool:
+ """Check if shape has any issues (overflow, overlap, or warnings)."""
+ return (
+ self.frame_overflow_bottom is not None
+ or self.slide_overflow_right is not None
+ or self.slide_overflow_bottom is not None
+ or len(self.overlapping_shapes) > 0
+ or len(self.warnings) > 0
+ )
+
+ def to_dict(self) -> ShapeDict:
+ """Convert to dictionary for JSON serialization."""
+ result: ShapeDict = {
+ "left": self.left,
+ "top": self.top,
+ "width": self.width,
+ "height": self.height,
+ }
+
+ # Add optional fields if present
+ if self.placeholder_type:
+ result["placeholder_type"] = self.placeholder_type
+
+ if self.default_font_size:
+ result["default_font_size"] = self.default_font_size
+
+ # Add overflow information only if there is overflow
+ overflow_data = {}
+
+ # Add frame overflow if present
+ if self.frame_overflow_bottom is not None:
+ overflow_data["frame"] = {"overflow_bottom": self.frame_overflow_bottom}
+
+ # Add slide overflow if present
+ slide_overflow = {}
+ if self.slide_overflow_right is not None:
+ slide_overflow["overflow_right"] = self.slide_overflow_right
+ if self.slide_overflow_bottom is not None:
+ slide_overflow["overflow_bottom"] = self.slide_overflow_bottom
+ if slide_overflow:
+ overflow_data["slide"] = slide_overflow
+
+ # Only add overflow field if there is overflow
+ if overflow_data:
+ result["overflow"] = overflow_data
+
+ # Add overlap field if there are overlapping shapes
+ if self.overlapping_shapes:
+ result["overlap"] = {"overlapping_shapes": self.overlapping_shapes}
+
+ # Add warnings field if there are warnings
+ if self.warnings:
+ result["warnings"] = self.warnings
+
+ # Add paragraphs after placeholder_type
+ result["paragraphs"] = [para.to_dict() for para in self.paragraphs]
+
+ return result
+
+
+def is_valid_shape(shape: BaseShape) -> bool:
+ """Check if a shape contains meaningful text content."""
+ # Must have a text frame with content
+ if not hasattr(shape, "text_frame") or not shape.text_frame: # type: ignore
+ return False
+
+ text = shape.text_frame.text.strip() # type: ignore
+ if not text:
+ return False
+
+ # Skip slide numbers and numeric footers
+ if hasattr(shape, "is_placeholder") and shape.is_placeholder: # type: ignore
+ if shape.placeholder_format and shape.placeholder_format.type: # type: ignore
+ placeholder_type = (
+ str(shape.placeholder_format.type).split(".")[-1].split(" ")[0] # type: ignore
+ )
+ if placeholder_type == "SLIDE_NUMBER":
+ return False
+ if placeholder_type == "FOOTER" and text.isdigit():
+ return False
+
+ return True
+
+
+def collect_shapes_with_absolute_positions(
+ shape: BaseShape, parent_left: int = 0, parent_top: int = 0
+) -> List[ShapeWithPosition]:
+ """Recursively collect all shapes with valid text, calculating absolute positions.
+
+ For shapes within groups, their positions are relative to the group.
+ This function calculates the absolute position on the slide by accumulating
+ parent group offsets.
+
+ Args:
+ shape: The shape to process
+ parent_left: Accumulated left offset from parent groups (in EMUs)
+ parent_top: Accumulated top offset from parent groups (in EMUs)
+
+ Returns:
+ List of ShapeWithPosition objects with absolute positions
+ """
+ if hasattr(shape, "shapes"): # GroupShape
+ result = []
+ # Get this group''s position
+ group_left = shape.left if hasattr(shape, "left") else 0
+ group_top = shape.top if hasattr(shape, "top") else 0
+
+ # Calculate absolute position for this group
+ abs_group_left = parent_left + group_left
+ abs_group_top = parent_top + group_top
+
+ # Process children with accumulated offsets
+ for child in shape.shapes: # type: ignore
+ result.extend(
+ collect_shapes_with_absolute_positions(
+ child, abs_group_left, abs_group_top
+ )
+ )
+ return result
+
+ # Regular shape - check if it has valid text
+ if is_valid_shape(shape):
+ # Calculate absolute position
+ shape_left = shape.left if hasattr(shape, "left") else 0
+ shape_top = shape.top if hasattr(shape, "top") else 0
+
+ return [
+ ShapeWithPosition(
+ shape=shape,
+ absolute_left=parent_left + shape_left,
+ absolute_top=parent_top + shape_top,
+ )
+ ]
+
+ return []
+
+
+def sort_shapes_by_position(shapes: List[ShapeData]) -> List[ShapeData]:
+ """Sort shapes by visual position (top-to-bottom, left-to-right).
+
+ Shapes within 0.5 inches vertically are considered on the same row.
+ """
+ if not shapes:
+ return shapes
+
+ # Sort by top position first
+ shapes = sorted(shapes, key=lambda s: (s.top, s.left))
+
+ # Group shapes by row (within 0.5 inches vertically)
+ result = []
+ row = [shapes[0]]
+ row_top = shapes[0].top
+
+ for shape in shapes[1:]:
+ if abs(shape.top - row_top) <= 0.5:
+ row.append(shape)
+ else:
+ # Sort current row by left position and add to result
+ result.extend(sorted(row, key=lambda s: s.left))
+ row = [shape]
+ row_top = shape.top
+
+ # Don''t forget the last row
+ result.extend(sorted(row, key=lambda s: s.left))
+ return result
+
+
+def calculate_overlap(
+ rect1: Tuple[float, float, float, float],
+ rect2: Tuple[float, float, float, float],
+ tolerance: float = 0.05,
+) -> Tuple[bool, float]:
+ """Calculate if and how much two rectangles overlap.
+
+ Args:
+ rect1: (left, top, width, height) of first rectangle in inches
+ rect2: (left, top, width, height) of second rectangle in inches
+ tolerance: Minimum overlap in inches to consider as overlapping (default: 0.05")
+
+ Returns:
+ Tuple of (overlaps, overlap_area) where:
+ - overlaps: True if rectangles overlap by more than tolerance
+ - overlap_area: Area of overlap in square inches
+ """
+ left1, top1, w1, h1 = rect1
+ left2, top2, w2, h2 = rect2
+
+ # Calculate overlap dimensions
+ overlap_width = min(left1 + w1, left2 + w2) - max(left1, left2)
+ overlap_height = min(top1 + h1, top2 + h2) - max(top1, top2)
+
+ # Check if there''s meaningful overlap (more than tolerance)
+ if overlap_width > tolerance and overlap_height > tolerance:
+ # Calculate overlap area in square inches
+ overlap_area = overlap_width * overlap_height
+ return True, round(overlap_area, 2)
+
+ return False, 0
+
+
+def detect_overlaps(shapes: List[ShapeData]) -> None:
+ """Detect overlapping shapes and update their overlapping_shapes dictionaries.
+
+ This function requires each ShapeData to have its shape_id already set.
+ It modifies the shapes in-place, adding shape IDs with overlap areas in square inches.
+
+ Args:
+ shapes: List of ShapeData objects with shape_id attributes set
+ """
+ n = len(shapes)
+
+ # Compare each pair of shapes
+ for i in range(n):
+ for j in range(i + 1, n):
+ shape1 = shapes[i]
+ shape2 = shapes[j]
+
+ # Ensure shape IDs are set
+ assert shape1.shape_id, f"Shape at index {i} has no shape_id"
+ assert shape2.shape_id, f"Shape at index {j} has no shape_id"
+
+ rect1 = (shape1.left, shape1.top, shape1.width, shape1.height)
+ rect2 = (shape2.left, shape2.top, shape2.width, shape2.height)
+
+ overlaps, overlap_area = calculate_overlap(rect1, rect2)
+
+ if overlaps:
+ # Add shape IDs with overlap area in square inches
+ shape1.overlapping_shapes[shape2.shape_id] = overlap_area
+ shape2.overlapping_shapes[shape1.shape_id] = overlap_area
+
+
+def extract_text_inventory(
+ pptx_path: Path, prs: Optional[Any] = None, issues_only: bool = False
+) -> InventoryData:
+ """Extract text content from all slides in a PowerPoint presentation.
+
+ Args:
+ pptx_path: Path to the PowerPoint file
+ prs: Optional Presentation object to use. If not provided, will load from pptx_path.
+ issues_only: If True, only include shapes that have overflow or overlap issues
+
+ Returns a nested dictionary: {slide-N: {shape-N: ShapeData}}
+ Shapes are sorted by visual position (top-to-bottom, left-to-right).
+ The ShapeData objects contain the full shape information and can be
+ converted to dictionaries for JSON serialization using to_dict().
+ """
+ if prs is None:
+ prs = Presentation(str(pptx_path))
+ inventory: InventoryData = {}
+
+ for slide_idx, slide in enumerate(prs.slides):
+ # Collect all valid shapes from this slide with absolute positions
+ shapes_with_positions = []
+ for shape in slide.shapes: # type: ignore
+ shapes_with_positions.extend(collect_shapes_with_absolute_positions(shape))
+
+ if not shapes_with_positions:
+ continue
+
+ # Convert to ShapeData with absolute positions and slide reference
+ shape_data_list = [
+ ShapeData(
+ swp.shape,
+ swp.absolute_left,
+ swp.absolute_top,
+ slide,
+ )
+ for swp in shapes_with_positions
+ ]
+
+ # Sort by visual position and assign stable IDs in one step
+ sorted_shapes = sort_shapes_by_position(shape_data_list)
+ for idx, shape_data in enumerate(sorted_shapes):
+ shape_data.shape_id = f"shape-{idx}"
+
+ # Detect overlaps using the stable shape IDs
+ if len(sorted_shapes) > 1:
+ detect_overlaps(sorted_shapes)
+
+ # Filter for issues only if requested (after overlap detection)
+ if issues_only:
+ sorted_shapes = [sd for sd in sorted_shapes if sd.has_any_issues]
+
+ if not sorted_shapes:
+ continue
+
+ # Create slide inventory using the stable shape IDs
+ inventory[f"slide-{slide_idx}"] = {
+ shape_data.shape_id: shape_data for shape_data in sorted_shapes
+ }
+
+ return inventory
+
+
+def get_inventory_as_dict(pptx_path: Path, issues_only: bool = False) -> InventoryDict:
+ """Extract text inventory and return as JSON-serializable dictionaries.
+
+ This is a convenience wrapper around extract_text_inventory that returns
+ dictionaries instead of ShapeData objects, useful for testing and direct
+ JSON serialization.
+
+ Args:
+ pptx_path: Path to the PowerPoint file
+ issues_only: If True, only include shapes that have overflow or overlap issues
+
+ Returns:
+ Nested dictionary with all data serialized for JSON
+ """
+ inventory = extract_text_inventory(pptx_path, issues_only=issues_only)
+
+ # Convert ShapeData objects to dictionaries
+ dict_inventory: InventoryDict = {}
+ for slide_key, shapes in inventory.items():
+ dict_inventory[slide_key] = {
+ shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items()
+ }
+
+ return dict_inventory
+
+
+def save_inventory(inventory: InventoryData, output_path: Path) -> None:
+ """Save inventory to JSON file with proper formatting.
+
+ Converts ShapeData objects to dictionaries for JSON serialization.
+ """
+ # Convert ShapeData objects to dictionaries
+ json_inventory: InventoryDict = {}
+ for slide_key, shapes in inventory.items():
+ json_inventory[slide_key] = {
+ shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items()
+ }
+
+ with open(output_path, "w", encoding="utf-8") as f:
+ json.dump(json_inventory, f, indent=2, ensure_ascii=False)
+
+
+if __name__ == "__main__":
+ main()
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'scripts/rearrange.py', E'#!/usr/bin/env python3
+"""
+Rearrange PowerPoint slides based on a sequence of indices.
+
+Usage:
+ python rearrange.py template.pptx output.pptx 0,34,34,50,52
+
+This will create output.pptx using slides from template.pptx in the specified order.
+Slides can be repeated (e.g., 34 appears twice).
+"""
+
+import argparse
+import shutil
+import sys
+from copy import deepcopy
+from pathlib import Path
+
+import six
+from pptx import Presentation
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Rearrange PowerPoint slides based on a sequence of indices.",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ python rearrange.py template.pptx output.pptx 0,34,34,50,52
+ Creates output.pptx using slides 0, 34 (twice), 50, and 52 from template.pptx
+
+ python rearrange.py template.pptx output.pptx 5,3,1,2,4
+ Creates output.pptx with slides reordered as specified
+
+Note: Slide indices are 0-based (first slide is 0, second is 1, etc.)
+ """,
+ )
+
+ parser.add_argument("template", help="Path to template PPTX file")
+ parser.add_argument("output", help="Path for output PPTX file")
+ parser.add_argument(
+ "sequence", help="Comma-separated sequence of slide indices (0-based)"
+ )
+
+ args = parser.parse_args()
+
+ # Parse the slide sequence
+ try:
+ slide_sequence = [int(x.strip()) for x in args.sequence.split(",")]
+ except ValueError:
+ print(
+ "Error: Invalid sequence format. Use comma-separated integers (e.g., 0,34,34,50,52)"
+ )
+ sys.exit(1)
+
+ # Check template exists
+ template_path = Path(args.template)
+ if not template_path.exists():
+ print(f"Error: Template file not found: {args.template}")
+ sys.exit(1)
+
+ # Create output directory if needed
+ output_path = Path(args.output)
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+
+ try:
+ rearrange_presentation(template_path, output_path, slide_sequence)
+ except ValueError as e:
+ print(f"Error: {e}")
+ sys.exit(1)
+ except Exception as e:
+ print(f"Error processing presentation: {e}")
+ sys.exit(1)
+
+
+def duplicate_slide(pres, index):
+ """Duplicate a slide in the presentation."""
+ source = pres.slides[index]
+
+ # Use source''s layout to preserve formatting
+ new_slide = pres.slides.add_slide(source.slide_layout)
+
+ # Collect all image and media relationships from the source slide
+ image_rels = {}
+ for rel_id, rel in six.iteritems(source.part.rels):
+ if "image" in rel.reltype or "media" in rel.reltype:
+ image_rels[rel_id] = rel
+
+ # CRITICAL: Clear placeholder shapes to avoid duplicates
+ for shape in new_slide.shapes:
+ sp = shape.element
+ sp.getparent().remove(sp)
+
+ # Copy all shapes from source
+ for shape in source.shapes:
+ el = shape.element
+ new_el = deepcopy(el)
+ new_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
+
+ # Handle picture shapes - need to update the blip reference
+ # Look for all blip elements (they can be in pic or other contexts)
+ # Using the element''s own xpath method without namespaces argument
+ blips = new_el.xpath(".//a:blip[@r:embed]")
+ for blip in blips:
+ old_rId = blip.get(
+ "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed"
+ )
+ if old_rId in image_rels:
+ # Create a new relationship in the destination slide for this image
+ old_rel = image_rels[old_rId]
+ # get_or_add returns the rId directly, or adds and returns new rId
+ new_rId = new_slide.part.rels.get_or_add(
+ old_rel.reltype, old_rel._target
+ )
+ # Update the blip''s embed reference to use the new relationship ID
+ blip.set(
+ "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed",
+ new_rId,
+ )
+
+ # Copy any additional image/media relationships that might be referenced elsewhere
+ for rel_id, rel in image_rels.items():
+ try:
+ new_slide.part.rels.get_or_add(rel.reltype, rel._target)
+ except Exception:
+ pass # Relationship might already exist
+
+ return new_slide
+
+
+def delete_slide(pres, index):
+ """Delete a slide from the presentation."""
+ rId = pres.slides._sldIdLst[index].rId
+ pres.part.drop_rel(rId)
+ del pres.slides._sldIdLst[index]
+
+
+def reorder_slides(pres, slide_index, target_index):
+ """Move a slide from one position to another."""
+ slides = pres.slides._sldIdLst
+
+ # Remove slide element from current position
+ slide_element = slides[slide_index]
+ slides.remove(slide_element)
+
+ # Insert at target position
+ slides.insert(target_index, slide_element)
+
+
+def rearrange_presentation(template_path, output_path, slide_sequence):
+ """
+ Create a new presentation with slides from template in specified order.
+
+ Args:
+ template_path: Path to template PPTX file
+ output_path: Path for output PPTX file
+ slide_sequence: List of slide indices (0-based) to include
+ """
+ # Copy template to preserve dimensions and theme
+ if template_path != output_path:
+ shutil.copy2(template_path, output_path)
+ prs = Presentation(output_path)
+ else:
+ prs = Presentation(template_path)
+
+ total_slides = len(prs.slides)
+
+ # Validate indices
+ for idx in slide_sequence:
+ if idx < 0 or idx >= total_slides:
+ raise ValueError(f"Slide index {idx} out of range (0-{total_slides - 1})")
+
+ # Track original slides and their duplicates
+ slide_map = [] # List of actual slide indices for final presentation
+ duplicated = {} # Track duplicates: original_idx -> [duplicate_indices]
+
+ # Step 1: DUPLICATE repeated slides
+ print(f"Processing {len(slide_sequence)} slides from template...")
+ for i, template_idx in enumerate(slide_sequence):
+ if template_idx in duplicated and duplicated[template_idx]:
+ # Already duplicated this slide, use the duplicate
+ slide_map.append(duplicated[template_idx].pop(0))
+ print(f" [{i}] Using duplicate of slide {template_idx}")
+ elif slide_sequence.count(template_idx) > 1 and template_idx not in duplicated:
+ # First occurrence of a repeated slide - create duplicates
+ slide_map.append(template_idx)
+ duplicates = []
+ count = slide_sequence.count(template_idx) - 1
+ print(
+ f" [{i}] Using original slide {template_idx}, creating {count} duplicate(s)"
+ )
+ for _ in range(count):
+ duplicate_slide(prs, template_idx)
+ duplicates.append(len(prs.slides) - 1)
+ duplicated[template_idx] = duplicates
+ else:
+ # Unique slide or first occurrence already handled, use original
+ slide_map.append(template_idx)
+ print(f" [{i}] Using original slide {template_idx}")
+
+ # Step 2: DELETE unwanted slides (work backwards)
+ slides_to_keep = set(slide_map)
+ print(f"\\nDeleting {len(prs.slides) - len(slides_to_keep)} unused slides...")
+ for i in range(len(prs.slides) - 1, -1, -1):
+ if i not in slides_to_keep:
+ delete_slide(prs, i)
+ # Update slide_map indices after deletion
+ slide_map = [idx - 1 if idx > i else idx for idx in slide_map]
+
+ # Step 3: REORDER to final sequence
+ print(f"Reordering {len(slide_map)} slides to final sequence...")
+ for target_pos in range(len(slide_map)):
+ # Find which slide should be at target_pos
+ current_pos = slide_map[target_pos]
+ if current_pos != target_pos:
+ reorder_slides(prs, current_pos, target_pos)
+ # Update slide_map: the move shifts other slides
+ for i in range(len(slide_map)):
+ if slide_map[i] > current_pos and slide_map[i] <= target_pos:
+ slide_map[i] -= 1
+ elif slide_map[i] < current_pos and slide_map[i] >= target_pos:
+ slide_map[i] += 1
+ slide_map[target_pos] = target_pos
+
+ # Save the presentation
+ prs.save(output_path)
+ print(f"\\nSaved rearranged presentation to: {output_path}")
+ print(f"Final presentation has {len(prs.slides)} slides")
+
+
+if __name__ == "__main__":
+ main()
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'scripts/replace.py', E'#!/usr/bin/env python3
+"""Apply text replacements to PowerPoint presentation.
+
+Usage:
+ python replace.py
+
+The replacements JSON should have the structure output by inventory.py.
+ALL text shapes identified by inventory.py will have their text cleared
+unless "paragraphs" is specified in the replacements for that shape.
+"""
+
+import json
+import sys
+from pathlib import Path
+from typing import Any, Dict, List
+
+from inventory import InventoryData, extract_text_inventory
+from pptx import Presentation
+from pptx.dml.color import RGBColor
+from pptx.enum.dml import MSO_THEME_COLOR
+from pptx.enum.text import PP_ALIGN
+from pptx.oxml.xmlchemy import OxmlElement
+from pptx.util import Pt
+
+
+def clear_paragraph_bullets(paragraph):
+ """Clear bullet formatting from a paragraph."""
+ pPr = paragraph._element.get_or_add_pPr()
+
+ # Remove existing bullet elements
+ for child in list(pPr):
+ if (
+ child.tag.endswith("buChar")
+ or child.tag.endswith("buNone")
+ or child.tag.endswith("buAutoNum")
+ or child.tag.endswith("buFont")
+ ):
+ pPr.remove(child)
+
+ return pPr
+
+
+def apply_paragraph_properties(paragraph, para_data: Dict[str, Any]):
+ """Apply formatting properties to a paragraph."""
+ # Get the text but don''t set it on paragraph directly yet
+ text = para_data.get("text", "")
+
+ # Get or create paragraph properties
+ pPr = clear_paragraph_bullets(paragraph)
+
+ # Handle bullet formatting
+ if para_data.get("bullet", False):
+ level = para_data.get("level", 0)
+ paragraph.level = level
+
+ # Calculate font-proportional indentation
+ font_size = para_data.get("font_size", 18.0)
+ level_indent_emu = int((font_size * (1.6 + level * 1.6)) * 12700)
+ hanging_indent_emu = int(-font_size * 0.8 * 12700)
+
+ # Set indentation
+ pPr.attrib["marL"] = str(level_indent_emu)
+ pPr.attrib["indent"] = str(hanging_indent_emu)
+
+ # Add bullet character
+ buChar = OxmlElement("a:buChar")
+ buChar.set("char", "•")
+ pPr.append(buChar)
+
+ # Default to left alignment for bullets if not specified
+ if "alignment" not in para_data:
+ paragraph.alignment = PP_ALIGN.LEFT
+ else:
+ # Remove indentation for non-bullet text
+ pPr.attrib["marL"] = "0"
+ pPr.attrib["indent"] = "0"
+
+ # Add buNone element
+ buNone = OxmlElement("a:buNone")
+ pPr.insert(0, buNone)
+
+ # Apply alignment
+ if "alignment" in para_data:
+ alignment_map = {
+ "LEFT": PP_ALIGN.LEFT,
+ "CENTER": PP_ALIGN.CENTER,
+ "RIGHT": PP_ALIGN.RIGHT,
+ "JUSTIFY": PP_ALIGN.JUSTIFY,
+ }
+ if para_data["alignment"] in alignment_map:
+ paragraph.alignment = alignment_map[para_data["alignment"]]
+
+ # Apply spacing
+ if "space_before" in para_data:
+ paragraph.space_before = Pt(para_data["space_before"])
+ if "space_after" in para_data:
+ paragraph.space_after = Pt(para_data["space_after"])
+ if "line_spacing" in para_data:
+ paragraph.line_spacing = Pt(para_data["line_spacing"])
+
+ # Apply run-level formatting
+ if not paragraph.runs:
+ run = paragraph.add_run()
+ run.text = text
+ else:
+ run = paragraph.runs[0]
+ run.text = text
+
+ # Apply font properties
+ apply_font_properties(run, para_data)
+
+
+def apply_font_properties(run, para_data: Dict[str, Any]):
+ """Apply font properties to a text run."""
+ if "bold" in para_data:
+ run.font.bold = para_data["bold"]
+ if "italic" in para_data:
+ run.font.italic = para_data["italic"]
+ if "underline" in para_data:
+ run.font.underline = para_data["underline"]
+ if "font_size" in para_data:
+ run.font.size = Pt(para_data["font_size"])
+ if "font_name" in para_data:
+ run.font.name = para_data["font_name"]
+
+ # Apply color - prefer RGB, fall back to theme_color
+ if "color" in para_data:
+ color_hex = para_data["color"].lstrip("#")
+ if len(color_hex) == 6:
+ r = int(color_hex[0:2], 16)
+ g = int(color_hex[2:4], 16)
+ b = int(color_hex[4:6], 16)
+ run.font.color.rgb = RGBColor(r, g, b)
+ elif "theme_color" in para_data:
+ # Get theme color by name (e.g., "DARK_1", "ACCENT_1")
+ theme_name = para_data["theme_color"]
+ try:
+ run.font.color.theme_color = getattr(MSO_THEME_COLOR, theme_name)
+ except AttributeError:
+ print(f" WARNING: Unknown theme color name ''{theme_name}''")
+
+
+def detect_frame_overflow(inventory: InventoryData) -> Dict[str, Dict[str, float]]:
+ """Detect text overflow in shapes (text exceeding shape bounds).
+
+ Returns dict of slide_key -> shape_key -> overflow_inches.
+ Only includes shapes that have text overflow.
+ """
+ overflow_map = {}
+
+ for slide_key, shapes_dict in inventory.items():
+ for shape_key, shape_data in shapes_dict.items():
+ # Check for frame overflow (text exceeding shape bounds)
+ if shape_data.frame_overflow_bottom is not None:
+ if slide_key not in overflow_map:
+ overflow_map[slide_key] = {}
+ overflow_map[slide_key][shape_key] = shape_data.frame_overflow_bottom
+
+ return overflow_map
+
+
+def validate_replacements(inventory: InventoryData, replacements: Dict) -> List[str]:
+ """Validate that all shapes in replacements exist in inventory.
+
+ Returns list of error messages.
+ """
+ errors = []
+
+ for slide_key, shapes_data in replacements.items():
+ if not slide_key.startswith("slide-"):
+ continue
+
+ # Check if slide exists
+ if slide_key not in inventory:
+ errors.append(f"Slide ''{slide_key}'' not found in inventory")
+ continue
+
+ # Check each shape
+ for shape_key in shapes_data.keys():
+ if shape_key not in inventory[slide_key]:
+ # Find shapes without replacements defined and show their content
+ unused_with_content = []
+ for k in inventory[slide_key].keys():
+ if k not in shapes_data:
+ shape_data = inventory[slide_key][k]
+ # Get text from paragraphs as preview
+ paragraphs = shape_data.paragraphs
+ if paragraphs and paragraphs[0].text:
+ first_text = paragraphs[0].text[:50]
+ if len(paragraphs[0].text) > 50:
+ first_text += "..."
+ unused_with_content.append(f"{k} (''{first_text}'')")
+ else:
+ unused_with_content.append(k)
+
+ errors.append(
+ f"Shape ''{shape_key}'' not found on ''{slide_key}''. "
+ f"Shapes without replacements: {'', ''.join(sorted(unused_with_content)) if unused_with_content else ''none''}"
+ )
+
+ return errors
+
+
+def check_duplicate_keys(pairs):
+ """Check for duplicate keys when loading JSON."""
+ result = {}
+ for key, value in pairs:
+ if key in result:
+ raise ValueError(f"Duplicate key found in JSON: ''{key}''")
+ result[key] = value
+ return result
+
+
+def apply_replacements(pptx_file: str, json_file: str, output_file: str):
+ """Apply text replacements from JSON to PowerPoint presentation."""
+
+ # Load presentation
+ prs = Presentation(pptx_file)
+
+ # Get inventory of all text shapes (returns ShapeData objects)
+ # Pass prs to use same Presentation instance
+ inventory = extract_text_inventory(Path(pptx_file), prs)
+
+ # Detect text overflow in original presentation
+ original_overflow = detect_frame_overflow(inventory)
+
+ # Load replacement data with duplicate key detection
+ with open(json_file, "r") as f:
+ replacements = json.load(f, object_pairs_hook=check_duplicate_keys)
+
+ # Validate replacements
+ errors = validate_replacements(inventory, replacements)
+ if errors:
+ print("ERROR: Invalid shapes in replacement JSON:")
+ for error in errors:
+ print(f" - {error}")
+ print("\\nPlease check the inventory and update your replacement JSON.")
+ print(
+ "You can regenerate the inventory with: python inventory.py "
+ )
+ raise ValueError(f"Found {len(errors)} validation error(s)")
+
+ # Track statistics
+ shapes_processed = 0
+ shapes_cleared = 0
+ shapes_replaced = 0
+
+ # Process each slide from inventory
+ for slide_key, shapes_dict in inventory.items():
+ if not slide_key.startswith("slide-"):
+ continue
+
+ slide_index = int(slide_key.split("-")[1])
+
+ if slide_index >= len(prs.slides):
+ print(f"Warning: Slide {slide_index} not found")
+ continue
+
+ # Process each shape from inventory
+ for shape_key, shape_data in shapes_dict.items():
+ shapes_processed += 1
+
+ # Get the shape directly from ShapeData
+ shape = shape_data.shape
+ if not shape:
+ print(f"Warning: {shape_key} has no shape reference")
+ continue
+
+ # ShapeData already validates text_frame in __init__
+ text_frame = shape.text_frame # type: ignore
+
+ text_frame.clear() # type: ignore
+ shapes_cleared += 1
+
+ # Check for replacement paragraphs
+ replacement_shape_data = replacements.get(slide_key, {}).get(shape_key, {})
+ if "paragraphs" not in replacement_shape_data:
+ continue
+
+ shapes_replaced += 1
+
+ # Add replacement paragraphs
+ for i, para_data in enumerate(replacement_shape_data["paragraphs"]):
+ if i == 0:
+ p = text_frame.paragraphs[0] # type: ignore
+ else:
+ p = text_frame.add_paragraph() # type: ignore
+
+ apply_paragraph_properties(p, para_data)
+
+ # Check for issues after replacements
+ # Save to a temporary file and reload to avoid modifying the presentation during inventory
+ # (extract_text_inventory accesses font.color which adds empty elements)
+ import tempfile
+
+ with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as tmp:
+ tmp_path = Path(tmp.name)
+ prs.save(str(tmp_path))
+
+ try:
+ updated_inventory = extract_text_inventory(tmp_path)
+ updated_overflow = detect_frame_overflow(updated_inventory)
+ finally:
+ tmp_path.unlink() # Clean up temp file
+
+ # Check if any text overflow got worse
+ overflow_errors = []
+ for slide_key, shape_overflows in updated_overflow.items():
+ for shape_key, new_overflow in shape_overflows.items():
+ # Get original overflow (0 if there was no overflow before)
+ original = original_overflow.get(slide_key, {}).get(shape_key, 0.0)
+
+ # Error if overflow increased
+ if new_overflow > original + 0.01: # Small tolerance for rounding
+ increase = new_overflow - original
+ overflow_errors.append(
+ f''{slide_key}/{shape_key}: overflow worsened by {increase:.2f}" ''
+ f''(was {original:.2f}", now {new_overflow:.2f}")''
+ )
+
+ # Collect warnings from updated shapes
+ warnings = []
+ for slide_key, shapes_dict in updated_inventory.items():
+ for shape_key, shape_data in shapes_dict.items():
+ if shape_data.warnings:
+ for warning in shape_data.warnings:
+ warnings.append(f"{slide_key}/{shape_key}: {warning}")
+
+ # Fail if there are any issues
+ if overflow_errors or warnings:
+ print("\\nERROR: Issues detected in replacement output:")
+ if overflow_errors:
+ print("\\nText overflow worsened:")
+ for error in overflow_errors:
+ print(f" - {error}")
+ if warnings:
+ print("\\nFormatting warnings:")
+ for warning in warnings:
+ print(f" - {warning}")
+ print("\\nPlease fix these issues before saving.")
+ raise ValueError(
+ f"Found {len(overflow_errors)} overflow error(s) and {len(warnings)} warning(s)"
+ )
+
+ # Save the presentation
+ prs.save(output_file)
+
+ # Report results
+ print(f"Saved updated presentation to: {output_file}")
+ print(f"Processed {len(prs.slides)} slides")
+ print(f" - Shapes processed: {shapes_processed}")
+ print(f" - Shapes cleared: {shapes_cleared}")
+ print(f" - Shapes replaced: {shapes_replaced}")
+
+
+def main():
+ """Main entry point for command-line usage."""
+ if len(sys.argv) != 4:
+ print(__doc__)
+ sys.exit(1)
+
+ input_pptx = Path(sys.argv[1])
+ replacements_json = Path(sys.argv[2])
+ output_pptx = Path(sys.argv[3])
+
+ if not input_pptx.exists():
+ print(f"Error: Input file ''{input_pptx}'' not found")
+ sys.exit(1)
+
+ if not replacements_json.exists():
+ print(f"Error: Replacements JSON file ''{replacements_json}'' not found")
+ sys.exit(1)
+
+ try:
+ apply_replacements(str(input_pptx), str(replacements_json), str(output_pptx))
+ except Exception as e:
+ print(f"Error applying replacements: {e}")
+ import traceback
+
+ traceback.print_exc()
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'scripts/thumbnail.py', '#!/usr/bin/env python3
+"""
+Create thumbnail grids from PowerPoint presentation slides.
+
+Creates a grid layout of slide thumbnails with configurable columns (max 6).
+Each grid contains up to cols×(cols+1) images. For presentations with more
+slides, multiple numbered grid files are created automatically.
+
+The program outputs the names of all files created.
+
+Output:
+- Single grid: {prefix}.jpg (if slides fit in one grid)
+- Multiple grids: {prefix}-1.jpg, {prefix}-2.jpg, etc.
+
+Grid limits by column count:
+- 3 cols: max 12 slides per grid (3×4)
+- 4 cols: max 20 slides per grid (4×5)
+- 5 cols: max 30 slides per grid (5×6) [default]
+- 6 cols: max 42 slides per grid (6×7)
+
+Usage:
+ python thumbnail.py input.pptx [output_prefix] [--cols N] [--outline-placeholders]
+
+Examples:
+ python thumbnail.py presentation.pptx
+ # Creates: thumbnails.jpg (using default prefix)
+ # Outputs:
+ # Created 1 grid(s):
+ # - thumbnails.jpg
+
+ python thumbnail.py large-deck.pptx grid --cols 4
+ # Creates: grid-1.jpg, grid-2.jpg, grid-3.jpg
+ # Outputs:
+ # Created 3 grid(s):
+ # - grid-1.jpg
+ # - grid-2.jpg
+ # - grid-3.jpg
+
+ python thumbnail.py template.pptx analysis --outline-placeholders
+ # Creates thumbnail grids with red outlines around text placeholders
+"""
+
+import argparse
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+from inventory import extract_text_inventory
+from PIL import Image, ImageDraw, ImageFont
+from pptx import Presentation
+
+# Constants
+THUMBNAIL_WIDTH = 300 # Fixed thumbnail width in pixels
+CONVERSION_DPI = 100 # DPI for PDF to image conversion
+MAX_COLS = 6 # Maximum number of columns
+DEFAULT_COLS = 5 # Default number of columns
+JPEG_QUALITY = 95 # JPEG compression quality
+
+# Grid layout constants
+GRID_PADDING = 20 # Padding between thumbnails
+BORDER_WIDTH = 2 # Border width around thumbnails
+FONT_SIZE_RATIO = 0.12 # Font size as fraction of thumbnail width
+LABEL_PADDING_RATIO = 0.4 # Label padding as fraction of font size
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Create thumbnail grids from PowerPoint slides."
+ )
+ parser.add_argument("input", help="Input PowerPoint file (.pptx)")
+ parser.add_argument(
+ "output_prefix",
+ nargs="?",
+ default="thumbnails",
+ help="Output prefix for image files (default: thumbnails, will create prefix.jpg or prefix-N.jpg)",
+ )
+ parser.add_argument(
+ "--cols",
+ type=int,
+ default=DEFAULT_COLS,
+ help=f"Number of columns (default: {DEFAULT_COLS}, max: {MAX_COLS})",
+ )
+ parser.add_argument(
+ "--outline-placeholders",
+ action="store_true",
+ help="Outline text placeholders with a colored border",
+ )
+
+ args = parser.parse_args()
+
+ # Validate columns
+ cols = min(args.cols, MAX_COLS)
+ if args.cols > MAX_COLS:
+ print(f"Warning: Columns limited to {MAX_COLS} (requested {args.cols})")
+
+ # Validate input
+ input_path = Path(args.input)
+ if not input_path.exists() or input_path.suffix.lower() != ".pptx":
+ print(f"Error: Invalid PowerPoint file: {args.input}")
+ sys.exit(1)
+
+ # Construct output path (always JPG)
+ output_path = Path(f"{args.output_prefix}.jpg")
+
+ print(f"Processing: {args.input}")
+
+ try:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ # Get placeholder regions if outlining is enabled
+ placeholder_regions = None
+ slide_dimensions = None
+ if args.outline_placeholders:
+ print("Extracting placeholder regions...")
+ placeholder_regions, slide_dimensions = get_placeholder_regions(
+ input_path
+ )
+ if placeholder_regions:
+ print(f"Found placeholders on {len(placeholder_regions)} slides")
+
+ # Convert slides to images
+ slide_images = convert_to_images(input_path, Path(temp_dir), CONVERSION_DPI)
+ if not slide_images:
+ print("Error: No slides found")
+ sys.exit(1)
+
+ print(f"Found {len(slide_images)} slides")
+
+ # Create grids (max cols×(cols+1) images per grid)
+ grid_files = create_grids(
+ slide_images,
+ cols,
+ THUMBNAIL_WIDTH,
+ output_path,
+ placeholder_regions,
+ slide_dimensions,
+ )
+
+ # Print saved files
+ print(f"Created {len(grid_files)} grid(s):")
+ for grid_file in grid_files:
+ print(f" - {grid_file}")
+
+ except Exception as e:
+ print(f"Error: {e}")
+ sys.exit(1)
+
+
+def create_hidden_slide_placeholder(size):
+ """Create placeholder image for hidden slides."""
+ img = Image.new("RGB", size, color="#F0F0F0")
+ draw = ImageDraw.Draw(img)
+ line_width = max(5, min(size) // 100)
+ draw.line([(0, 0), size], fill="#CCCCCC", width=line_width)
+ draw.line([(size[0], 0), (0, size[1])], fill="#CCCCCC", width=line_width)
+ return img
+
+
+def get_placeholder_regions(pptx_path):
+ """Extract ALL text regions from the presentation.
+
+ Returns a tuple of (placeholder_regions, slide_dimensions).
+ text_regions is a dict mapping slide indices to lists of text regions.
+ Each region is a dict with ''left'', ''top'', ''width'', ''height'' in inches.
+ slide_dimensions is a tuple of (width_inches, height_inches).
+ """
+ prs = Presentation(str(pptx_path))
+ inventory = extract_text_inventory(pptx_path, prs)
+ placeholder_regions = {}
+
+ # Get actual slide dimensions in inches (EMU to inches conversion)
+ slide_width_inches = (prs.slide_width or 9144000) / 914400.0
+ slide_height_inches = (prs.slide_height or 5143500) / 914400.0
+
+ for slide_key, shapes in inventory.items():
+ # Extract slide index from "slide-N" format
+ slide_idx = int(slide_key.split("-")[1])
+ regions = []
+
+ for shape_key, shape_data in shapes.items():
+ # The inventory only contains shapes with text, so all shapes should be highlighted
+ regions.append(
+ {
+ "left": shape_data.left,
+ "top": shape_data.top,
+ "width": shape_data.width,
+ "height": shape_data.height,
+ }
+ )
+
+ if regions:
+ placeholder_regions[slide_idx] = regions
+
+ return placeholder_regions, (slide_width_inches, slide_height_inches)
+
+
+def convert_to_images(pptx_path, temp_dir, dpi):
+ """Convert PowerPoint to images via PDF, handling hidden slides."""
+ # Detect hidden slides
+ print("Analyzing presentation...")
+ prs = Presentation(str(pptx_path))
+ total_slides = len(prs.slides)
+
+ # Find hidden slides (1-based indexing for display)
+ hidden_slides = {
+ idx + 1
+ for idx, slide in enumerate(prs.slides)
+ if slide.element.get("show") == "0"
+ }
+
+ print(f"Total slides: {total_slides}")
+ if hidden_slides:
+ print(f"Hidden slides: {sorted(hidden_slides)}")
+
+ pdf_path = temp_dir / f"{pptx_path.stem}.pdf"
+
+ # Convert to PDF
+ print("Converting to PDF...")
+ result = subprocess.run(
+ [
+ "soffice",
+ "--headless",
+ "--convert-to",
+ "pdf",
+ "--outdir",
+ str(temp_dir),
+ str(pptx_path),
+ ],
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode != 0 or not pdf_path.exists():
+ raise RuntimeError("PDF conversion failed")
+
+ # Convert PDF to images
+ print(f"Converting to images at {dpi} DPI...")
+ result = subprocess.run(
+ ["pdftoppm", "-jpeg", "-r", str(dpi), str(pdf_path), str(temp_dir / "slide")],
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode != 0:
+ raise RuntimeError("Image conversion failed")
+
+ visible_images = sorted(temp_dir.glob("slide-*.jpg"))
+
+ # Create full list with placeholders for hidden slides
+ all_images = []
+ visible_idx = 0
+
+ # Get placeholder dimensions from first visible slide
+ if visible_images:
+ with Image.open(visible_images[0]) as img:
+ placeholder_size = img.size
+ else:
+ placeholder_size = (1920, 1080)
+
+ for slide_num in range(1, total_slides + 1):
+ if slide_num in hidden_slides:
+ # Create placeholder image for hidden slide
+ placeholder_path = temp_dir / f"hidden-{slide_num:03d}.jpg"
+ placeholder_img = create_hidden_slide_placeholder(placeholder_size)
+ placeholder_img.save(placeholder_path, "JPEG")
+ all_images.append(placeholder_path)
+ else:
+ # Use the actual visible slide image
+ if visible_idx < len(visible_images):
+ all_images.append(visible_images[visible_idx])
+ visible_idx += 1
+
+ return all_images
+
+
+def create_grids(
+ image_paths,
+ cols,
+ width,
+ output_path,
+ placeholder_regions=None,
+ slide_dimensions=None,
+):
+ """Create multiple thumbnail grids from slide images, max cols×(cols+1) images per grid."""
+ # Maximum images per grid is cols × (cols + 1) for better proportions
+ max_images_per_grid = cols * (cols + 1)
+ grid_files = []
+
+ print(
+ f"Creating grids with {cols} columns (max {max_images_per_grid} images per grid)"
+ )
+
+ # Split images into chunks
+ for chunk_idx, start_idx in enumerate(
+ range(0, len(image_paths), max_images_per_grid)
+ ):
+ end_idx = min(start_idx + max_images_per_grid, len(image_paths))
+ chunk_images = image_paths[start_idx:end_idx]
+
+ # Create grid for this chunk
+ grid = create_grid(
+ chunk_images, cols, width, start_idx, placeholder_regions, slide_dimensions
+ )
+
+ # Generate output filename
+ if len(image_paths) <= max_images_per_grid:
+ # Single grid - use base filename without suffix
+ grid_filename = output_path
+ else:
+ # Multiple grids - insert index before extension with dash
+ stem = output_path.stem
+ suffix = output_path.suffix
+ grid_filename = output_path.parent / f"{stem}-{chunk_idx + 1}{suffix}"
+
+ # Save grid
+ grid_filename.parent.mkdir(parents=True, exist_ok=True)
+ grid.save(str(grid_filename), quality=JPEG_QUALITY)
+ grid_files.append(str(grid_filename))
+
+ return grid_files
+
+
+def create_grid(
+ image_paths,
+ cols,
+ width,
+ start_slide_num=0,
+ placeholder_regions=None,
+ slide_dimensions=None,
+):
+ """Create thumbnail grid from slide images with optional placeholder outlining."""
+ font_size = int(width * FONT_SIZE_RATIO)
+ label_padding = int(font_size * LABEL_PADDING_RATIO)
+
+ # Get dimensions
+ with Image.open(image_paths[0]) as img:
+ aspect = img.height / img.width
+ height = int(width * aspect)
+
+ # Calculate grid size
+ rows = (len(image_paths) + cols - 1) // cols
+ grid_w = cols * width + (cols + 1) * GRID_PADDING
+ grid_h = rows * (height + font_size + label_padding * 2) + (rows + 1) * GRID_PADDING
+
+ # Create grid
+ grid = Image.new("RGB", (grid_w, grid_h), "white")
+ draw = ImageDraw.Draw(grid)
+
+ # Load font with size based on thumbnail width
+ try:
+ # Use Pillow''s default font with size
+ font = ImageFont.load_default(size=font_size)
+ except Exception:
+ # Fall back to basic default font if size parameter not supported
+ font = ImageFont.load_default()
+
+ # Place thumbnails
+ for i, img_path in enumerate(image_paths):
+ row, col = i // cols, i % cols
+ x = col * width + (col + 1) * GRID_PADDING
+ y_base = (
+ row * (height + font_size + label_padding * 2) + (row + 1) * GRID_PADDING
+ )
+
+ # Add label with actual slide number
+ label = f"{start_slide_num + i}"
+ bbox = draw.textbbox((0, 0), label, font=font)
+ text_w = bbox[2] - bbox[0]
+ draw.text(
+ (x + (width - text_w) // 2, y_base + label_padding),
+ label,
+ fill="black",
+ font=font,
+ )
+
+ # Add thumbnail below label with proportional spacing
+ y_thumbnail = y_base + label_padding + font_size + label_padding
+
+ with Image.open(img_path) as img:
+ # Get original dimensions before thumbnail
+ orig_w, orig_h = img.size
+
+ # Apply placeholder outlines if enabled
+ if placeholder_regions and (start_slide_num + i) in placeholder_regions:
+ # Convert to RGBA for transparency support
+ if img.mode != "RGBA":
+ img = img.convert("RGBA")
+
+ # Get the regions for this slide
+ regions = placeholder_regions[start_slide_num + i]
+
+ # Calculate scale factors using actual slide dimensions
+ if slide_dimensions:
+ slide_width_inches, slide_height_inches = slide_dimensions
+ else:
+ # Fallback: estimate from image size at CONVERSION_DPI
+ slide_width_inches = orig_w / CONVERSION_DPI
+ slide_height_inches = orig_h / CONVERSION_DPI
+
+ x_scale = orig_w / slide_width_inches
+ y_scale = orig_h / slide_height_inches
+
+ # Create a highlight overlay
+ overlay = Image.new("RGBA", img.size, (255, 255, 255, 0))
+ overlay_draw = ImageDraw.Draw(overlay)
+
+ # Highlight each placeholder region
+ for region in regions:
+ # Convert from inches to pixels in the original image
+ px_left = int(region["left"] * x_scale)
+ px_top = int(region["top"] * y_scale)
+ px_width = int(region["width"] * x_scale)
+ px_height = int(region["height"] * y_scale)
+
+ # Draw highlight outline with red color and thick stroke
+ # Using a bright red outline instead of fill
+ stroke_width = max(
+ 5, min(orig_w, orig_h) // 150
+ ) # Thicker proportional stroke width
+ overlay_draw.rectangle(
+ [(px_left, px_top), (px_left + px_width, px_top + px_height)],
+ outline=(255, 0, 0, 255), # Bright red, fully opaque
+ width=stroke_width,
+ )
+
+ # Composite the overlay onto the image using alpha blending
+ img = Image.alpha_composite(img, overlay)
+ # Convert back to RGB for JPEG saving
+ img = img.convert("RGB")
+
+ img.thumbnail((width, height), Image.Resampling.LANCZOS)
+ w, h = img.size
+ tx = x + (width - w) // 2
+ ty = y_thumbnail + (height - h) // 2
+ grid.paste(img, (tx, ty))
+
+ # Add border
+ if BORDER_WIDTH > 0:
+ draw.rectangle(
+ [
+ (tx - BORDER_WIDTH, ty - BORDER_WIDTH),
+ (tx + w + BORDER_WIDTH - 1, ty + h + BORDER_WIDTH - 1),
+ ],
+ outline="gray",
+ width=BORDER_WIDTH,
+ )
+
+ return grid
+
+
+if __name__ == "__main__":
+ main()
+' FROM agentscope.agentscope_skills s WHERE s.name = 'pptx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'references/scenario-rules.md', '# 先进级场景诊断规则
+
+场景名称以当年度正式附件为准。以下清单来自 2026 年江苏省典型场景企业自评价参考;政策更新时先替换清单和达标描述,再开展诊断。
+
+## 40 个标准场景
+
+- 工厂建设(3):工厂数字化规划设计、数字基础设施建设、数字孪生工厂构建。
+- 产品研发(2):产品数字化设计、产品虚拟验证。
+- 工艺设计(2):工艺数字化设计、制造工程优化。
+- 生产管理(12):生产计划优化、车间智能排产、生产进度跟踪、生产动态调度、仓储智能管理、物料精准配送、危险作业自动化、安全一体化管控、能源智能管控、碳资产全生命周期管理、污染在线管控、网络协同制造。
+- 生产作业(10):柔性产线快速换产、工艺动态优化、先进过程控制、人机协同作业、在线智能检测、质量精准追溯、质量分析与改进、设备运行监控、设备故障诊断与预测、设备维修维护。
+- 运营管理(4):智能经营决策、数智精益管理、规模化定制、产品精准营销。
+- 产品服务(3):远程运维服务、产品增值服务、客户主动服务。
+- 供应链管理(4):供应商数字化管理、采购计划优化协同、供应链风险预警与调度、供应链物流智能配送。
+
+申报书五方面映射:工厂建设对应工厂建设;研发设计对应产品研发和工艺设计;生产作业对应生产作业;生产管理对应生产管理;运营管理对应运营管理、产品服务和供应链管理。
+
+## 判定字段
+
+每个场景至少记录:标准名称、所属环节、对应申报方面、先进级达标描述、企业实例、关键装备、关键软件或系统、关键工艺或技术、量化成效、图片或截图、来源位置、AI 场景映射、状态。
+
+三档判定:
+
+- 已达标且有佐证:建设内容达到先进级描述,关键事实和数值可定位到材料。
+- 有建设但佐证不足:存在相关系统或业务描述,但缺达标要素、参数、效果或证据。
+- 未建设可规划:没有现状证据;仅在规划依据充分时提出建设方向,并明确标为规划。
+
+硬校验:至少 15 个场景,且覆盖生产作业、生产管理、运营管理三个环节。AI 场景以当年度 AI 自评文件映射;无正式映射时标“无”或“待确认”,不得自拟标准结论。
+' FROM agentscope.agentscope_skills s WHERE s.name = 'smart-factory-gap-analysis' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'LICENSE.txt', '© 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.
+' FROM agentscope.agentscope_skills s WHERE s.name = 'xlsx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO agentscope.agentscope_skill_resources(id, resource_path, resource_content) SELECT s.id, 'recalc.py', E'#!/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 = ''''''
+
+
+ Sub RecalculateAndSave()
+ ThisComponent.calculateAll()
+ ThisComponent.store()
+ ThisComponent.close(True)
+ End Sub
+''''''
+
+ 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 [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()' FROM agentscope.agentscope_skills s WHERE s.name = 'xlsx' ON CONFLICT (id, resource_path) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'advanced-smart-factory-review', 'v1.0', 'BUILTIN', TRUE, TRUE, '5558860937d3c7a7bc9c1594affb76ac24d0ff399f03e57550e01de014e31346', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'advanced-smart-factory-review') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'alibabacloud-bailian-rag-knowledgebase', 'v1.0', 'BUILTIN', TRUE, TRUE, '1b46319c17a24e1359b017bab86be448dd53610385ec8c8a19ad838e59413970', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'alibabacloud-bailian-rag-knowledgebase') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'docx', 'v1.0', 'BUILTIN', TRUE, TRUE, 'd2b7270817e00f94b72a172d73556ec6b1d247c92d28f65b57bd6786bc63bf64', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'docx') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'excellent-smart-factory-review', 'v1.0', 'BUILTIN', TRUE, TRUE, '6c5bf4fefac3aa54785a0ede4b5ed7add563eadc2e19134e6da907d0d42efcae', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'excellent-smart-factory-review') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'pdf', 'v1.0', 'IMPORTED', TRUE, TRUE, '3a8902b4e8c1a838e399cfdbbaeb1298e709c1a8811b7ca7bdf6f59eba21fbc9', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'pdf') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'pptx', 'v1.0', 'IMPORTED', TRUE, TRUE, '950291d19b3fc4df6f900de429460498f4028414d13e5ddfa2005c8133377801', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'pptx') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'smart-factory-company-profile', 'v1.0', 'BUILTIN', TRUE, TRUE, 'ece4e59d8ef1b452af18829d7eaed10bee9486a5ae48b2a8200f23b605210550', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'smart-factory-company-profile') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'smart-factory-construction-review', 'v1.0', 'BUILTIN', TRUE, TRUE, 'ab9e6b17e482d3d11f1135f6035632159c41bb0b56ffb9b84d8204e8a8e2fb13', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'smart-factory-construction-review') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'smart-factory-construction-writing', 'v1.0', 'BUILTIN', TRUE, TRUE, '4500b9e4904aab838d9900a44fe6f791630bef45729f021e460ef3dabc309555', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'smart-factory-construction-writing') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'smart-factory-gap-analysis', 'v1.0', 'BUILTIN', TRUE, TRUE, 'ab908f38013572d4e6fff1d447f1335c08671d2f630e1df84b82480faa75f3be', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'smart-factory-gap-analysis') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'smart-factory-planning', 'v1.0', 'BUILTIN', TRUE, TRUE, '6c6e9e52e0dc623de2c7377b899b957b17f0ffe76b92151ea9977fe3f8a4967d', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'smart-factory-planning') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'smart-factory-summary-writing', 'v1.0', 'BUILTIN', TRUE, TRUE, '171c259920259c44008e96b22de705dfc2c050e7531121bb9de1ea0396b1bc85', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'smart-factory-summary-writing') ON CONFLICT (skill_name) DO NOTHING;
+INSERT INTO app.skill_config(skill_name, version, source_type, enabled, read_only, checksum, validation_status, validation_message, imported_by) SELECT 'xlsx', 'v1.0', 'IMPORTED', TRUE, TRUE, '87ed0bc26905cbfcc58963a00b382cede937e41316a8699dc926842e989b23e4', 'VALID', NULL, NULL WHERE EXISTS (SELECT 1 FROM agentscope.agentscope_skills s WHERE s.name = 'xlsx') ON CONFLICT (skill_name) DO NOTHING;
diff --git a/server/src/main/resources/db/migration/V2__seed_skills.sql.conf b/server/src/main/resources/db/migration/V2__seed_skills.sql.conf
new file mode 100644
index 0000000..a6180c5
--- /dev/null
+++ b/server/src/main/resources/db/migration/V2__seed_skills.sql.conf
@@ -0,0 +1 @@
+placeholderReplacement=false
diff --git a/server/src/test/java/cn/alphaline/smartfactory/DatabaseAndEventIntegrationTest.java b/server/src/test/java/tech/easyflow/manuagent/DatabaseAndEventIntegrationTest.java
similarity index 96%
rename from server/src/test/java/cn/alphaline/smartfactory/DatabaseAndEventIntegrationTest.java
rename to server/src/test/java/tech/easyflow/manuagent/DatabaseAndEventIntegrationTest.java
index 9650fe1..54058bd 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/DatabaseAndEventIntegrationTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/DatabaseAndEventIntegrationTest.java
@@ -1,14 +1,15 @@
-package cn.alphaline.smartfactory;
+package tech.easyflow.manuagent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import cn.alphaline.smartfactory.agent.AgentEventService;
-import cn.alphaline.smartfactory.artifact.ArtifactService;
-import cn.alphaline.smartfactory.artifact.DocxValidator;
-import cn.alphaline.smartfactory.project.ProjectService;
-import cn.alphaline.smartfactory.project.ProjectFileService;
+import tech.easyflow.manuagent.agent.AgentEventService;
+import tech.easyflow.manuagent.artifact.ArtifactService;
+import tech.easyflow.manuagent.artifact.DocxValidator;
+import tech.easyflow.manuagent.auth.UserService;
+import tech.easyflow.manuagent.project.ProjectService;
+import tech.easyflow.manuagent.project.ProjectFileService;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -212,7 +213,7 @@ class DatabaseAndEventIntegrationTest {
""").param("id", UUID.randomUUID()).param("projectId", projectId).param("runId", runId)
.param("sha", "0".repeat(64)).update();
- ProjectService service = new ProjectService(jdbc, mock(cn.alphaline.smartfactory.auth.UserService.class), new ObjectMapper());
+ ProjectService service = new ProjectService(jdbc, mock(UserService.class), new ObjectMapper());
service.delete(projectId);
for (String table : List.of("agent_event", "artifact", "project_plan", "project_file", "agent_run")) {
diff --git a/server/src/test/java/cn/alphaline/smartfactory/KeyCipherAndShellTest.java b/server/src/test/java/tech/easyflow/manuagent/KeyCipherAndShellTest.java
similarity index 87%
rename from server/src/test/java/cn/alphaline/smartfactory/KeyCipherAndShellTest.java
rename to server/src/test/java/tech/easyflow/manuagent/KeyCipherAndShellTest.java
index 8791d6e..01fe2f6 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/KeyCipherAndShellTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/KeyCipherAndShellTest.java
@@ -1,9 +1,9 @@
-package cn.alphaline.smartfactory;
+package tech.easyflow.manuagent;
import static org.assertj.core.api.Assertions.assertThat;
-import cn.alphaline.smartfactory.config.AppProperties;
-import cn.alphaline.smartfactory.model.KeyCipher;
+import tech.easyflow.manuagent.config.AppProperties;
+import tech.easyflow.manuagent.model.KeyCipher;
import java.nio.file.Path;
import java.time.Duration;
import org.junit.jupiter.api.Test;
diff --git a/server/src/test/java/cn/alphaline/smartfactory/agent/AgentExecutionServiceTest.java b/server/src/test/java/tech/easyflow/manuagent/agent/AgentExecutionServiceTest.java
similarity index 95%
rename from server/src/test/java/cn/alphaline/smartfactory/agent/AgentExecutionServiceTest.java
rename to server/src/test/java/tech/easyflow/manuagent/agent/AgentExecutionServiceTest.java
index cf3e17e..35590b2 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/agent/AgentExecutionServiceTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/agent/AgentExecutionServiceTest.java
@@ -1,4 +1,4 @@
-package cn.alphaline.smartfactory.agent;
+package tech.easyflow.manuagent.agent;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/server/src/test/java/cn/alphaline/smartfactory/agent/AgentRunServiceTest.java b/server/src/test/java/tech/easyflow/manuagent/agent/AgentRunServiceTest.java
similarity index 98%
rename from server/src/test/java/cn/alphaline/smartfactory/agent/AgentRunServiceTest.java
rename to server/src/test/java/tech/easyflow/manuagent/agent/AgentRunServiceTest.java
index ecc4717..efb93ea 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/agent/AgentRunServiceTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/agent/AgentRunServiceTest.java
@@ -1,4 +1,4 @@
-package cn.alphaline.smartfactory.agent;
+package tech.easyflow.manuagent.agent;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/server/src/test/java/cn/alphaline/smartfactory/agent/DocumentViewToolTest.java b/server/src/test/java/tech/easyflow/manuagent/agent/DocumentViewToolTest.java
similarity index 95%
rename from server/src/test/java/cn/alphaline/smartfactory/agent/DocumentViewToolTest.java
rename to server/src/test/java/tech/easyflow/manuagent/agent/DocumentViewToolTest.java
index f9eedeb..4143d01 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/agent/DocumentViewToolTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/agent/DocumentViewToolTest.java
@@ -1,4 +1,4 @@
-package cn.alphaline.smartfactory.agent;
+package tech.easyflow.manuagent.agent;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/server/src/test/java/cn/alphaline/smartfactory/agent/PagedReadFileToolTest.java b/server/src/test/java/tech/easyflow/manuagent/agent/PagedReadFileToolTest.java
similarity index 98%
rename from server/src/test/java/cn/alphaline/smartfactory/agent/PagedReadFileToolTest.java
rename to server/src/test/java/tech/easyflow/manuagent/agent/PagedReadFileToolTest.java
index d1aeb2c..a5102f2 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/agent/PagedReadFileToolTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/agent/PagedReadFileToolTest.java
@@ -1,4 +1,4 @@
-package cn.alphaline.smartfactory.agent;
+package tech.easyflow.manuagent.agent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
diff --git a/server/src/test/java/cn/alphaline/smartfactory/artifact/DocxValidatorTest.java b/server/src/test/java/tech/easyflow/manuagent/artifact/DocxValidatorTest.java
similarity index 98%
rename from server/src/test/java/cn/alphaline/smartfactory/artifact/DocxValidatorTest.java
rename to server/src/test/java/tech/easyflow/manuagent/artifact/DocxValidatorTest.java
index 8284d14..307a57f 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/artifact/DocxValidatorTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/artifact/DocxValidatorTest.java
@@ -1,9 +1,9 @@
-package cn.alphaline.smartfactory.artifact;
+package tech.easyflow.manuagent.artifact;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import cn.alphaline.smartfactory.common.ApiException;
+import tech.easyflow.manuagent.common.ApiException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
diff --git a/server/src/test/java/cn/alphaline/smartfactory/auth/AuthControllerTest.java b/server/src/test/java/tech/easyflow/manuagent/auth/AuthControllerTest.java
similarity index 94%
rename from server/src/test/java/cn/alphaline/smartfactory/auth/AuthControllerTest.java
rename to server/src/test/java/tech/easyflow/manuagent/auth/AuthControllerTest.java
index 29e7bb2..f29af05 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/auth/AuthControllerTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/auth/AuthControllerTest.java
@@ -1,4 +1,4 @@
-package cn.alphaline.smartfactory.auth;
+package tech.easyflow.manuagent.auth;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -7,7 +7,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import cn.alphaline.smartfactory.common.GlobalExceptionHandler;
+import tech.easyflow.manuagent.common.GlobalExceptionHandler;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
diff --git a/server/src/test/java/cn/alphaline/smartfactory/common/GlobalExceptionHandlerTest.java b/server/src/test/java/tech/easyflow/manuagent/common/GlobalExceptionHandlerTest.java
similarity index 94%
rename from server/src/test/java/cn/alphaline/smartfactory/common/GlobalExceptionHandlerTest.java
rename to server/src/test/java/tech/easyflow/manuagent/common/GlobalExceptionHandlerTest.java
index 08c0034..65feef6 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/common/GlobalExceptionHandlerTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/common/GlobalExceptionHandlerTest.java
@@ -1,4 +1,4 @@
-package cn.alphaline.smartfactory.common;
+package tech.easyflow.manuagent.common;
import static org.assertj.core.api.Assertions.assertThatCode;
diff --git a/server/src/test/java/cn/alphaline/smartfactory/project/ProjectFileServiceTest.java b/server/src/test/java/tech/easyflow/manuagent/project/ProjectFileServiceTest.java
similarity index 90%
rename from server/src/test/java/cn/alphaline/smartfactory/project/ProjectFileServiceTest.java
rename to server/src/test/java/tech/easyflow/manuagent/project/ProjectFileServiceTest.java
index 0524160..0c5b642 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/project/ProjectFileServiceTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/project/ProjectFileServiceTest.java
@@ -1,11 +1,12 @@
-package cn.alphaline.smartfactory.project;
+package tech.easyflow.manuagent.project;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
-import cn.alphaline.smartfactory.auth.UserService;
-import cn.alphaline.smartfactory.config.AppProperties;
+import tech.easyflow.manuagent.auth.UserService;
+import tech.easyflow.manuagent.common.ApiException;
+import tech.easyflow.manuagent.config.AppProperties;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
@@ -66,9 +67,9 @@ class ProjectFileServiceTest {
ProjectFileService service = service();
assertThatThrownBy(() -> service.normalizeUploadPath("../企业材料.pdf", "企业材料.pdf"))
- .isInstanceOf(cn.alphaline.smartfactory.common.ApiException.class);
+ .isInstanceOf(ApiException.class);
assertThatThrownBy(() -> service.normalizeUploadPath("其他文件.pdf", "企业材料.pdf"))
- .isInstanceOf(cn.alphaline.smartfactory.common.ApiException.class);
+ .isInstanceOf(ApiException.class);
}
/**
diff --git a/server/src/test/java/cn/alphaline/smartfactory/skill/SkillArchiveTest.java b/server/src/test/java/tech/easyflow/manuagent/skill/SkillArchiveTest.java
similarity index 98%
rename from server/src/test/java/cn/alphaline/smartfactory/skill/SkillArchiveTest.java
rename to server/src/test/java/tech/easyflow/manuagent/skill/SkillArchiveTest.java
index 14e814f..61e55cc 100644
--- a/server/src/test/java/cn/alphaline/smartfactory/skill/SkillArchiveTest.java
+++ b/server/src/test/java/tech/easyflow/manuagent/skill/SkillArchiveTest.java
@@ -1,4 +1,4 @@
-package cn.alphaline.smartfactory.skill;
+package tech.easyflow.manuagent.skill;
import static org.assertj.core.api.Assertions.assertThat;