@heylemon/lemonade 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/web/inbound/monitor.js +0 -1
- package/package.json +1 -1
- package/skills/docx/SKILL.md +20 -464
- package/skills/docx/references/templates.md +46 -0
- package/skills/docx/scripts/create_doc.py +252 -0
- package/skills/docx/scripts/validate_doc.py +75 -0
- package/skills/pptx/SKILL.md +7 -215
- package/skills/pptx/references/spec-format.md +54 -0
- package/skills/pptx/scripts/create_pptx.js +218 -0
- package/skills/xlsx/SKILL.md +12 -281
- package/skills/xlsx/references/spec-format.md +53 -0
- package/skills/xlsx/scripts/create_xlsx.py +222 -0
- package/skills/xlsx/scripts/validate_xlsx.py +73 -0
- package/skills/pptx/editing.md +0 -205
- package/skills/pptx/pptxgenjs.md +0 -420
- package/skills/pptx/scripts/add_slide.py +0 -195
- package/skills/pptx/scripts/clean.py +0 -286
- package/skills/pptx/scripts/thumbnail.py +0 -289
package/skills/pptx/pptxgenjs.md
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
# PptxGenJS Tutorial
|
|
2
|
-
|
|
3
|
-
## Setup & Basic Structure
|
|
4
|
-
|
|
5
|
-
```javascript
|
|
6
|
-
const pptxgen = require("pptxgenjs");
|
|
7
|
-
|
|
8
|
-
let pres = new pptxgen();
|
|
9
|
-
pres.layout = 'LAYOUT_16x9'; // or 'LAYOUT_16x10', 'LAYOUT_4x3', 'LAYOUT_WIDE'
|
|
10
|
-
pres.author = 'Your Name';
|
|
11
|
-
pres.title = 'Presentation Title';
|
|
12
|
-
|
|
13
|
-
let slide = pres.addSlide();
|
|
14
|
-
slide.addText("Hello World!", { x: 0.5, y: 0.5, fontSize: 36, color: "363636" });
|
|
15
|
-
|
|
16
|
-
pres.writeFile({ fileName: "Presentation.pptx" });
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Layout Dimensions
|
|
20
|
-
|
|
21
|
-
Slide dimensions (coordinates in inches):
|
|
22
|
-
- `LAYOUT_16x9`: 10" × 5.625" (default)
|
|
23
|
-
- `LAYOUT_16x10`: 10" × 6.25"
|
|
24
|
-
- `LAYOUT_4x3`: 10" × 7.5"
|
|
25
|
-
- `LAYOUT_WIDE`: 13.3" × 7.5"
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Text & Formatting
|
|
30
|
-
|
|
31
|
-
```javascript
|
|
32
|
-
// Basic text
|
|
33
|
-
slide.addText("Simple Text", {
|
|
34
|
-
x: 1, y: 1, w: 8, h: 2, fontSize: 24, fontFace: "Arial",
|
|
35
|
-
color: "363636", bold: true, align: "center", valign: "middle"
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Character spacing (use charSpacing, not letterSpacing which is silently ignored)
|
|
39
|
-
slide.addText("SPACED TEXT", { x: 1, y: 1, w: 8, h: 1, charSpacing: 6 });
|
|
40
|
-
|
|
41
|
-
// Rich text arrays
|
|
42
|
-
slide.addText([
|
|
43
|
-
{ text: "Bold ", options: { bold: true } },
|
|
44
|
-
{ text: "Italic ", options: { italic: true } }
|
|
45
|
-
], { x: 1, y: 3, w: 8, h: 1 });
|
|
46
|
-
|
|
47
|
-
// Multi-line text (requires breakLine: true)
|
|
48
|
-
slide.addText([
|
|
49
|
-
{ text: "Line 1", options: { breakLine: true } },
|
|
50
|
-
{ text: "Line 2", options: { breakLine: true } },
|
|
51
|
-
{ text: "Line 3" } // Last item doesn't need breakLine
|
|
52
|
-
], { x: 0.5, y: 0.5, w: 8, h: 2 });
|
|
53
|
-
|
|
54
|
-
// Text box margin (internal padding)
|
|
55
|
-
slide.addText("Title", {
|
|
56
|
-
x: 0.5, y: 0.3, w: 9, h: 0.6,
|
|
57
|
-
margin: 0 // Use 0 when aligning text with other elements like shapes or icons
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
**Tip:** Text boxes have internal margin by default. Set `margin: 0` when you need text to align precisely with shapes, lines, or icons at the same x-position.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Lists & Bullets
|
|
66
|
-
|
|
67
|
-
```javascript
|
|
68
|
-
// ✅ CORRECT: Multiple bullets
|
|
69
|
-
slide.addText([
|
|
70
|
-
{ text: "First item", options: { bullet: true, breakLine: true } },
|
|
71
|
-
{ text: "Second item", options: { bullet: true, breakLine: true } },
|
|
72
|
-
{ text: "Third item", options: { bullet: true } }
|
|
73
|
-
], { x: 0.5, y: 0.5, w: 8, h: 3 });
|
|
74
|
-
|
|
75
|
-
// ❌ WRONG: Never use unicode bullets
|
|
76
|
-
slide.addText("• First item", { ... }); // Creates double bullets
|
|
77
|
-
|
|
78
|
-
// Sub-items and numbered lists
|
|
79
|
-
{ text: "Sub-item", options: { bullet: true, indentLevel: 1 } }
|
|
80
|
-
{ text: "First", options: { bullet: { type: "number" }, breakLine: true } }
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## Shapes
|
|
86
|
-
|
|
87
|
-
```javascript
|
|
88
|
-
slide.addShape(pres.shapes.RECTANGLE, {
|
|
89
|
-
x: 0.5, y: 0.8, w: 1.5, h: 3.0,
|
|
90
|
-
fill: { color: "FF0000" }, line: { color: "000000", width: 2 }
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
slide.addShape(pres.shapes.OVAL, { x: 4, y: 1, w: 2, h: 2, fill: { color: "0000FF" } });
|
|
94
|
-
|
|
95
|
-
slide.addShape(pres.shapes.LINE, {
|
|
96
|
-
x: 1, y: 3, w: 5, h: 0, line: { color: "FF0000", width: 3, dashType: "dash" }
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// With transparency
|
|
100
|
-
slide.addShape(pres.shapes.RECTANGLE, {
|
|
101
|
-
x: 1, y: 1, w: 3, h: 2,
|
|
102
|
-
fill: { color: "0088CC", transparency: 50 }
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Rounded rectangle (rectRadius only works with ROUNDED_RECTANGLE, not RECTANGLE)
|
|
106
|
-
// ⚠️ Don't pair with rectangular accent overlays — they won't cover rounded corners. Use RECTANGLE instead.
|
|
107
|
-
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
|
|
108
|
-
x: 1, y: 1, w: 3, h: 2,
|
|
109
|
-
fill: { color: "FFFFFF" }, rectRadius: 0.1
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// With shadow
|
|
113
|
-
slide.addShape(pres.shapes.RECTANGLE, {
|
|
114
|
-
x: 1, y: 1, w: 3, h: 2,
|
|
115
|
-
fill: { color: "FFFFFF" },
|
|
116
|
-
shadow: { type: "outer", color: "000000", blur: 6, offset: 2, angle: 135, opacity: 0.15 }
|
|
117
|
-
});
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Shadow options:
|
|
121
|
-
|
|
122
|
-
| Property | Type | Range | Notes |
|
|
123
|
-
|----------|------|-------|-------|
|
|
124
|
-
| `type` | string | `"outer"`, `"inner"` | |
|
|
125
|
-
| `color` | string | 6-char hex (e.g. `"000000"`) | No `#` prefix, no 8-char hex — see Common Pitfalls |
|
|
126
|
-
| `blur` | number | 0-100 pt | |
|
|
127
|
-
| `offset` | number | 0-200 pt | **Must be non-negative** — negative values corrupt the file |
|
|
128
|
-
| `angle` | number | 0-359 degrees | Direction the shadow falls (135 = bottom-right, 270 = upward) |
|
|
129
|
-
| `opacity` | number | 0.0-1.0 | Use this for transparency, never encode in color string |
|
|
130
|
-
|
|
131
|
-
To cast a shadow upward (e.g. on a footer bar), use `angle: 270` with a positive offset — do **not** use a negative offset.
|
|
132
|
-
|
|
133
|
-
**Note**: Gradient fills are not natively supported. Use a gradient image as a background instead.
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
## Images
|
|
138
|
-
|
|
139
|
-
### Image Sources
|
|
140
|
-
|
|
141
|
-
```javascript
|
|
142
|
-
// From file path
|
|
143
|
-
slide.addImage({ path: "images/chart.png", x: 1, y: 1, w: 5, h: 3 });
|
|
144
|
-
|
|
145
|
-
// From URL
|
|
146
|
-
slide.addImage({ path: "https://example.com/image.jpg", x: 1, y: 1, w: 5, h: 3 });
|
|
147
|
-
|
|
148
|
-
// From base64 (faster, no file I/O)
|
|
149
|
-
slide.addImage({ data: "image/png;base64,iVBORw0KGgo...", x: 1, y: 1, w: 5, h: 3 });
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Image Options
|
|
153
|
-
|
|
154
|
-
```javascript
|
|
155
|
-
slide.addImage({
|
|
156
|
-
path: "image.png",
|
|
157
|
-
x: 1, y: 1, w: 5, h: 3,
|
|
158
|
-
rotate: 45, // 0-359 degrees
|
|
159
|
-
rounding: true, // Circular crop
|
|
160
|
-
transparency: 50, // 0-100
|
|
161
|
-
flipH: true, // Horizontal flip
|
|
162
|
-
flipV: false, // Vertical flip
|
|
163
|
-
altText: "Description", // Accessibility
|
|
164
|
-
hyperlink: { url: "https://example.com" }
|
|
165
|
-
});
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Image Sizing Modes
|
|
169
|
-
|
|
170
|
-
```javascript
|
|
171
|
-
// Contain - fit inside, preserve ratio
|
|
172
|
-
{ sizing: { type: 'contain', w: 4, h: 3 } }
|
|
173
|
-
|
|
174
|
-
// Cover - fill area, preserve ratio (may crop)
|
|
175
|
-
{ sizing: { type: 'cover', w: 4, h: 3 } }
|
|
176
|
-
|
|
177
|
-
// Crop - cut specific portion
|
|
178
|
-
{ sizing: { type: 'crop', x: 0.5, y: 0.5, w: 2, h: 2 } }
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Calculate Dimensions (preserve aspect ratio)
|
|
182
|
-
|
|
183
|
-
```javascript
|
|
184
|
-
const origWidth = 1978, origHeight = 923, maxHeight = 3.0;
|
|
185
|
-
const calcWidth = maxHeight * (origWidth / origHeight);
|
|
186
|
-
const centerX = (10 - calcWidth) / 2;
|
|
187
|
-
|
|
188
|
-
slide.addImage({ path: "image.png", x: centerX, y: 1.2, w: calcWidth, h: maxHeight });
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Supported Formats
|
|
192
|
-
|
|
193
|
-
- **Standard**: PNG, JPG, GIF (animated GIFs work in Microsoft 365)
|
|
194
|
-
- **SVG**: Works in modern PowerPoint/Microsoft 365
|
|
195
|
-
|
|
196
|
-
---
|
|
197
|
-
|
|
198
|
-
## Icons
|
|
199
|
-
|
|
200
|
-
Use react-icons to generate SVG icons, then rasterize to PNG for universal compatibility.
|
|
201
|
-
|
|
202
|
-
### Setup
|
|
203
|
-
|
|
204
|
-
```javascript
|
|
205
|
-
const React = require("react");
|
|
206
|
-
const ReactDOMServer = require("react-dom/server");
|
|
207
|
-
const sharp = require("sharp");
|
|
208
|
-
const { FaCheckCircle, FaChartLine } = require("react-icons/fa");
|
|
209
|
-
|
|
210
|
-
function renderIconSvg(IconComponent, color = "#000000", size = 256) {
|
|
211
|
-
return ReactDOMServer.renderToStaticMarkup(
|
|
212
|
-
React.createElement(IconComponent, { color, size: String(size) })
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function iconToBase64Png(IconComponent, color, size = 256) {
|
|
217
|
-
const svg = renderIconSvg(IconComponent, color, size);
|
|
218
|
-
const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
|
|
219
|
-
return "image/png;base64," + pngBuffer.toString("base64");
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Add Icon to Slide
|
|
224
|
-
|
|
225
|
-
```javascript
|
|
226
|
-
const iconData = await iconToBase64Png(FaCheckCircle, "#4472C4", 256);
|
|
227
|
-
|
|
228
|
-
slide.addImage({
|
|
229
|
-
data: iconData,
|
|
230
|
-
x: 1, y: 1, w: 0.5, h: 0.5 // Size in inches
|
|
231
|
-
});
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**Note**: Use size 256 or higher for crisp icons. The size parameter controls the rasterization resolution, not the display size on the slide (which is set by `w` and `h` in inches).
|
|
235
|
-
|
|
236
|
-
### Icon Libraries
|
|
237
|
-
|
|
238
|
-
Install: `npm install -g react-icons react react-dom sharp`
|
|
239
|
-
|
|
240
|
-
Popular icon sets in react-icons:
|
|
241
|
-
- `react-icons/fa` - Font Awesome
|
|
242
|
-
- `react-icons/md` - Material Design
|
|
243
|
-
- `react-icons/hi` - Heroicons
|
|
244
|
-
- `react-icons/bi` - Bootstrap Icons
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
## Slide Backgrounds
|
|
249
|
-
|
|
250
|
-
```javascript
|
|
251
|
-
// Solid color
|
|
252
|
-
slide.background = { color: "F1F1F1" };
|
|
253
|
-
|
|
254
|
-
// Color with transparency
|
|
255
|
-
slide.background = { color: "FF3399", transparency: 50 };
|
|
256
|
-
|
|
257
|
-
// Image from URL
|
|
258
|
-
slide.background = { path: "https://example.com/bg.jpg" };
|
|
259
|
-
|
|
260
|
-
// Image from base64
|
|
261
|
-
slide.background = { data: "image/png;base64,iVBORw0KGgo..." };
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
---
|
|
265
|
-
|
|
266
|
-
## Tables
|
|
267
|
-
|
|
268
|
-
```javascript
|
|
269
|
-
slide.addTable([
|
|
270
|
-
["Header 1", "Header 2"],
|
|
271
|
-
["Cell 1", "Cell 2"]
|
|
272
|
-
], {
|
|
273
|
-
x: 1, y: 1, w: 8, h: 2,
|
|
274
|
-
border: { pt: 1, color: "999999" }, fill: { color: "F1F1F1" }
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Advanced with merged cells
|
|
278
|
-
let tableData = [
|
|
279
|
-
[{ text: "Header", options: { fill: { color: "6699CC" }, color: "FFFFFF", bold: true } }, "Cell"],
|
|
280
|
-
[{ text: "Merged", options: { colspan: 2 } }]
|
|
281
|
-
];
|
|
282
|
-
slide.addTable(tableData, { x: 1, y: 3.5, w: 8, colW: [4, 4] });
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
## Charts
|
|
288
|
-
|
|
289
|
-
```javascript
|
|
290
|
-
// Bar chart
|
|
291
|
-
slide.addChart(pres.charts.BAR, [{
|
|
292
|
-
name: "Sales", labels: ["Q1", "Q2", "Q3", "Q4"], values: [4500, 5500, 6200, 7100]
|
|
293
|
-
}], {
|
|
294
|
-
x: 0.5, y: 0.6, w: 6, h: 3, barDir: 'col',
|
|
295
|
-
showTitle: true, title: 'Quarterly Sales'
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// Line chart
|
|
299
|
-
slide.addChart(pres.charts.LINE, [{
|
|
300
|
-
name: "Temp", labels: ["Jan", "Feb", "Mar"], values: [32, 35, 42]
|
|
301
|
-
}], { x: 0.5, y: 4, w: 6, h: 3, lineSize: 3, lineSmooth: true });
|
|
302
|
-
|
|
303
|
-
// Pie chart
|
|
304
|
-
slide.addChart(pres.charts.PIE, [{
|
|
305
|
-
name: "Share", labels: ["A", "B", "Other"], values: [35, 45, 20]
|
|
306
|
-
}], { x: 7, y: 1, w: 5, h: 4, showPercent: true });
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Better-Looking Charts
|
|
310
|
-
|
|
311
|
-
Default charts look dated. Apply these options for a modern, clean appearance:
|
|
312
|
-
|
|
313
|
-
```javascript
|
|
314
|
-
slide.addChart(pres.charts.BAR, chartData, {
|
|
315
|
-
x: 0.5, y: 1, w: 9, h: 4, barDir: "col",
|
|
316
|
-
|
|
317
|
-
// Custom colors (match your presentation palette)
|
|
318
|
-
chartColors: ["0D9488", "14B8A6", "5EEAD4"],
|
|
319
|
-
|
|
320
|
-
// Clean background
|
|
321
|
-
chartArea: { fill: { color: "FFFFFF" }, roundedCorners: true },
|
|
322
|
-
|
|
323
|
-
// Muted axis labels
|
|
324
|
-
catAxisLabelColor: "64748B",
|
|
325
|
-
valAxisLabelColor: "64748B",
|
|
326
|
-
|
|
327
|
-
// Subtle grid (value axis only)
|
|
328
|
-
valGridLine: { color: "E2E8F0", size: 0.5 },
|
|
329
|
-
catGridLine: { style: "none" },
|
|
330
|
-
|
|
331
|
-
// Data labels on bars
|
|
332
|
-
showValue: true,
|
|
333
|
-
dataLabelPosition: "outEnd",
|
|
334
|
-
dataLabelColor: "1E293B",
|
|
335
|
-
|
|
336
|
-
// Hide legend for single series
|
|
337
|
-
showLegend: false,
|
|
338
|
-
});
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
**Key styling options:**
|
|
342
|
-
- `chartColors: [...]` - hex colors for series/segments
|
|
343
|
-
- `chartArea: { fill, border, roundedCorners }` - chart background
|
|
344
|
-
- `catGridLine/valGridLine: { color, style, size }` - grid lines (`style: "none"` to hide)
|
|
345
|
-
- `lineSmooth: true` - curved lines (line charts)
|
|
346
|
-
- `legendPos: "r"` - legend position: "b", "t", "l", "r", "tr"
|
|
347
|
-
|
|
348
|
-
---
|
|
349
|
-
|
|
350
|
-
## Slide Masters
|
|
351
|
-
|
|
352
|
-
```javascript
|
|
353
|
-
pres.defineSlideMaster({
|
|
354
|
-
title: 'TITLE_SLIDE', background: { color: '283A5E' },
|
|
355
|
-
objects: [{
|
|
356
|
-
placeholder: { options: { name: 'title', type: 'title', x: 1, y: 2, w: 8, h: 2 } }
|
|
357
|
-
}]
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
let titleSlide = pres.addSlide({ masterName: "TITLE_SLIDE" });
|
|
361
|
-
titleSlide.addText("My Title", { placeholder: "title" });
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
## Common Pitfalls
|
|
367
|
-
|
|
368
|
-
⚠️ These issues cause file corruption, visual bugs, or broken output. Avoid them.
|
|
369
|
-
|
|
370
|
-
1. **NEVER use "#" with hex colors** - causes file corruption
|
|
371
|
-
```javascript
|
|
372
|
-
color: "FF0000" // ✅ CORRECT
|
|
373
|
-
color: "#FF0000" // ❌ WRONG
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
2. **NEVER encode opacity in hex color strings** - 8-char colors (e.g., `"00000020"`) corrupt the file. Use the `opacity` property instead.
|
|
377
|
-
```javascript
|
|
378
|
-
shadow: { type: "outer", blur: 6, offset: 2, color: "00000020" } // ❌ CORRUPTS FILE
|
|
379
|
-
shadow: { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.12 } // ✅ CORRECT
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
3. **Use `bullet: true`** - NEVER unicode symbols like "•" (creates double bullets)
|
|
383
|
-
|
|
384
|
-
4. **Use `breakLine: true`** between array items or text runs together
|
|
385
|
-
|
|
386
|
-
5. **Avoid `lineSpacing` with bullets** - causes excessive gaps; use `paraSpaceAfter` instead
|
|
387
|
-
|
|
388
|
-
6. **Each presentation needs fresh instance** - don't reuse `pptxgen()` objects
|
|
389
|
-
|
|
390
|
-
7. **NEVER reuse option objects across calls** - PptxGenJS mutates objects in-place (e.g. converting shadow values to EMU). Sharing one object between multiple calls corrupts the second shape.
|
|
391
|
-
```javascript
|
|
392
|
-
const shadow = { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 };
|
|
393
|
-
slide.addShape(pres.shapes.RECTANGLE, { shadow, ... }); // ❌ second call gets already-converted values
|
|
394
|
-
slide.addShape(pres.shapes.RECTANGLE, { shadow, ... });
|
|
395
|
-
|
|
396
|
-
const makeShadow = () => ({ type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 });
|
|
397
|
-
slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... }); // ✅ fresh object each time
|
|
398
|
-
slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... });
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
8. **Don't use `ROUNDED_RECTANGLE` with accent borders** - rectangular overlay bars won't cover rounded corners. Use `RECTANGLE` instead.
|
|
402
|
-
```javascript
|
|
403
|
-
// ❌ WRONG: Accent bar doesn't cover rounded corners
|
|
404
|
-
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } });
|
|
405
|
-
slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } });
|
|
406
|
-
|
|
407
|
-
// ✅ CORRECT: Use RECTANGLE for clean alignment
|
|
408
|
-
slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } });
|
|
409
|
-
slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } });
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
---
|
|
413
|
-
|
|
414
|
-
## Quick Reference
|
|
415
|
-
|
|
416
|
-
- **Shapes**: RECTANGLE, OVAL, LINE, ROUNDED_RECTANGLE
|
|
417
|
-
- **Charts**: BAR, LINE, PIE, DOUGHNUT, SCATTER, BUBBLE, RADAR
|
|
418
|
-
- **Layouts**: LAYOUT_16x9 (10"×5.625"), LAYOUT_16x10, LAYOUT_4x3, LAYOUT_WIDE
|
|
419
|
-
- **Alignment**: "left", "center", "right"
|
|
420
|
-
- **Chart data labels**: "outEnd", "inEnd", "center"
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
"""Add a new slide to an unpacked PPTX directory.
|
|
2
|
-
|
|
3
|
-
Usage: python add_slide.py <unpacked_dir> <source>
|
|
4
|
-
|
|
5
|
-
The source can be:
|
|
6
|
-
- A slide file (e.g., slide2.xml) - duplicates the slide
|
|
7
|
-
- A layout file (e.g., slideLayout2.xml) - creates from layout
|
|
8
|
-
|
|
9
|
-
Examples:
|
|
10
|
-
python add_slide.py unpacked/ slide2.xml
|
|
11
|
-
# Duplicates slide2, creates slide5.xml
|
|
12
|
-
|
|
13
|
-
python add_slide.py unpacked/ slideLayout2.xml
|
|
14
|
-
# Creates slide5.xml from slideLayout2.xml
|
|
15
|
-
|
|
16
|
-
To see available layouts: ls unpacked/ppt/slideLayouts/
|
|
17
|
-
|
|
18
|
-
Prints the <p:sldId> element to add to presentation.xml.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import re
|
|
22
|
-
import shutil
|
|
23
|
-
import sys
|
|
24
|
-
from pathlib import Path
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def get_next_slide_number(slides_dir: Path) -> int:
|
|
28
|
-
existing = [int(m.group(1)) for f in slides_dir.glob("slide*.xml")
|
|
29
|
-
if (m := re.match(r"slide(\d+)\.xml", f.name))]
|
|
30
|
-
return max(existing) + 1 if existing else 1
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def create_slide_from_layout(unpacked_dir: Path, layout_file: str) -> None:
|
|
34
|
-
slides_dir = unpacked_dir / "ppt" / "slides"
|
|
35
|
-
rels_dir = slides_dir / "_rels"
|
|
36
|
-
layouts_dir = unpacked_dir / "ppt" / "slideLayouts"
|
|
37
|
-
|
|
38
|
-
layout_path = layouts_dir / layout_file
|
|
39
|
-
if not layout_path.exists():
|
|
40
|
-
print(f"Error: {layout_path} not found", file=sys.stderr)
|
|
41
|
-
sys.exit(1)
|
|
42
|
-
|
|
43
|
-
next_num = get_next_slide_number(slides_dir)
|
|
44
|
-
dest = f"slide{next_num}.xml"
|
|
45
|
-
dest_slide = slides_dir / dest
|
|
46
|
-
dest_rels = rels_dir / f"{dest}.rels"
|
|
47
|
-
|
|
48
|
-
slide_xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
49
|
-
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
|
50
|
-
<p:cSld>
|
|
51
|
-
<p:spTree>
|
|
52
|
-
<p:nvGrpSpPr>
|
|
53
|
-
<p:cNvPr id="1" name=""/>
|
|
54
|
-
<p:cNvGrpSpPr/>
|
|
55
|
-
<p:nvPr/>
|
|
56
|
-
</p:nvGrpSpPr>
|
|
57
|
-
<p:grpSpPr>
|
|
58
|
-
<a:xfrm>
|
|
59
|
-
<a:off x="0" y="0"/>
|
|
60
|
-
<a:ext cx="0" cy="0"/>
|
|
61
|
-
<a:chOff x="0" y="0"/>
|
|
62
|
-
<a:chExt cx="0" cy="0"/>
|
|
63
|
-
</a:xfrm>
|
|
64
|
-
</p:grpSpPr>
|
|
65
|
-
</p:spTree>
|
|
66
|
-
</p:cSld>
|
|
67
|
-
<p:clrMapOvr>
|
|
68
|
-
<a:masterClrMapping/>
|
|
69
|
-
</p:clrMapOvr>
|
|
70
|
-
</p:sld>'''
|
|
71
|
-
dest_slide.write_text(slide_xml, encoding="utf-8")
|
|
72
|
-
|
|
73
|
-
rels_dir.mkdir(exist_ok=True)
|
|
74
|
-
rels_xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
75
|
-
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
76
|
-
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/{layout_file}"/>
|
|
77
|
-
</Relationships>'''
|
|
78
|
-
dest_rels.write_text(rels_xml, encoding="utf-8")
|
|
79
|
-
|
|
80
|
-
_add_to_content_types(unpacked_dir, dest)
|
|
81
|
-
|
|
82
|
-
rid = _add_to_presentation_rels(unpacked_dir, dest)
|
|
83
|
-
|
|
84
|
-
next_slide_id = _get_next_slide_id(unpacked_dir)
|
|
85
|
-
|
|
86
|
-
print(f"Created {dest} from {layout_file}")
|
|
87
|
-
print(f'Add to presentation.xml <p:sldIdLst>: <p:sldId id="{next_slide_id}" r:id="{rid}"/>')
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def duplicate_slide(unpacked_dir: Path, source: str) -> None:
|
|
91
|
-
slides_dir = unpacked_dir / "ppt" / "slides"
|
|
92
|
-
rels_dir = slides_dir / "_rels"
|
|
93
|
-
|
|
94
|
-
source_slide = slides_dir / source
|
|
95
|
-
|
|
96
|
-
if not source_slide.exists():
|
|
97
|
-
print(f"Error: {source_slide} not found", file=sys.stderr)
|
|
98
|
-
sys.exit(1)
|
|
99
|
-
|
|
100
|
-
next_num = get_next_slide_number(slides_dir)
|
|
101
|
-
dest = f"slide{next_num}.xml"
|
|
102
|
-
dest_slide = slides_dir / dest
|
|
103
|
-
|
|
104
|
-
source_rels = rels_dir / f"{source}.rels"
|
|
105
|
-
dest_rels = rels_dir / f"{dest}.rels"
|
|
106
|
-
|
|
107
|
-
shutil.copy2(source_slide, dest_slide)
|
|
108
|
-
|
|
109
|
-
if source_rels.exists():
|
|
110
|
-
shutil.copy2(source_rels, dest_rels)
|
|
111
|
-
|
|
112
|
-
rels_content = dest_rels.read_text(encoding="utf-8")
|
|
113
|
-
rels_content = re.sub(
|
|
114
|
-
r'\s*<Relationship[^>]*Type="[^"]*notesSlide"[^>]*/>\s*',
|
|
115
|
-
"\n",
|
|
116
|
-
rels_content,
|
|
117
|
-
)
|
|
118
|
-
dest_rels.write_text(rels_content, encoding="utf-8")
|
|
119
|
-
|
|
120
|
-
_add_to_content_types(unpacked_dir, dest)
|
|
121
|
-
|
|
122
|
-
rid = _add_to_presentation_rels(unpacked_dir, dest)
|
|
123
|
-
|
|
124
|
-
next_slide_id = _get_next_slide_id(unpacked_dir)
|
|
125
|
-
|
|
126
|
-
print(f"Created {dest} from {source}")
|
|
127
|
-
print(f'Add to presentation.xml <p:sldIdLst>: <p:sldId id="{next_slide_id}" r:id="{rid}"/>')
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def _add_to_content_types(unpacked_dir: Path, dest: str) -> None:
|
|
131
|
-
content_types_path = unpacked_dir / "[Content_Types].xml"
|
|
132
|
-
content_types = content_types_path.read_text(encoding="utf-8")
|
|
133
|
-
|
|
134
|
-
new_override = f'<Override PartName="/ppt/slides/{dest}" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>'
|
|
135
|
-
|
|
136
|
-
if f"/ppt/slides/{dest}" not in content_types:
|
|
137
|
-
content_types = content_types.replace("</Types>", f" {new_override}\n</Types>")
|
|
138
|
-
content_types_path.write_text(content_types, encoding="utf-8")
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _add_to_presentation_rels(unpacked_dir: Path, dest: str) -> str:
|
|
142
|
-
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
|
143
|
-
pres_rels = pres_rels_path.read_text(encoding="utf-8")
|
|
144
|
-
|
|
145
|
-
rids = [int(m) for m in re.findall(r'Id="rId(\d+)"', pres_rels)]
|
|
146
|
-
next_rid = max(rids) + 1 if rids else 1
|
|
147
|
-
rid = f"rId{next_rid}"
|
|
148
|
-
|
|
149
|
-
new_rel = f'<Relationship Id="{rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/{dest}"/>'
|
|
150
|
-
|
|
151
|
-
if f"slides/{dest}" not in pres_rels:
|
|
152
|
-
pres_rels = pres_rels.replace("</Relationships>", f" {new_rel}\n</Relationships>")
|
|
153
|
-
pres_rels_path.write_text(pres_rels, encoding="utf-8")
|
|
154
|
-
|
|
155
|
-
return rid
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _get_next_slide_id(unpacked_dir: Path) -> int:
|
|
159
|
-
pres_path = unpacked_dir / "ppt" / "presentation.xml"
|
|
160
|
-
pres_content = pres_path.read_text(encoding="utf-8")
|
|
161
|
-
slide_ids = [int(m) for m in re.findall(r'<p:sldId[^>]*id="(\d+)"', pres_content)]
|
|
162
|
-
return max(slide_ids) + 1 if slide_ids else 256
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def parse_source(source: str) -> tuple[str, str | None]:
|
|
166
|
-
if source.startswith("slideLayout") and source.endswith(".xml"):
|
|
167
|
-
return ("layout", source)
|
|
168
|
-
|
|
169
|
-
return ("slide", None)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if __name__ == "__main__":
|
|
173
|
-
if len(sys.argv) != 3:
|
|
174
|
-
print("Usage: python add_slide.py <unpacked_dir> <source>", file=sys.stderr)
|
|
175
|
-
print("", file=sys.stderr)
|
|
176
|
-
print("Source can be:", file=sys.stderr)
|
|
177
|
-
print(" slide2.xml - duplicate an existing slide", file=sys.stderr)
|
|
178
|
-
print(" slideLayout2.xml - create from a layout template", file=sys.stderr)
|
|
179
|
-
print("", file=sys.stderr)
|
|
180
|
-
print("To see available layouts: ls <unpacked_dir>/ppt/slideLayouts/", file=sys.stderr)
|
|
181
|
-
sys.exit(1)
|
|
182
|
-
|
|
183
|
-
unpacked_dir = Path(sys.argv[1])
|
|
184
|
-
source = sys.argv[2]
|
|
185
|
-
|
|
186
|
-
if not unpacked_dir.exists():
|
|
187
|
-
print(f"Error: {unpacked_dir} not found", file=sys.stderr)
|
|
188
|
-
sys.exit(1)
|
|
189
|
-
|
|
190
|
-
source_type, layout_file = parse_source(source)
|
|
191
|
-
|
|
192
|
-
if source_type == "layout" and layout_file is not None:
|
|
193
|
-
create_slide_from_layout(unpacked_dir, layout_file)
|
|
194
|
-
else:
|
|
195
|
-
duplicate_slide(unpacked_dir, source)
|