@heylemon/lemonade 0.0.4 → 0.0.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/gateway/skills-http.js +74 -19
- package/package.json +1 -1
- package/skills/docx/SKILL.md +25 -30
- package/skills/docx/scripts/accept_changes.py +0 -17
- package/skills/docx/scripts/comment.py +10 -39
- package/skills/docx/scripts/office/helpers/merge_runs.py +1 -33
- package/skills/docx/scripts/office/helpers/simplify_redlines.py +0 -43
- package/skills/docx/scripts/office/pack.py +0 -30
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -1499
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -1085
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -3081
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -287
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -1676
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -174
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -582
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -4439
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -570
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -116
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -42
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -50
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -49
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -33
- package/skills/docx/scripts/office/soffice.py +0 -55
- package/skills/docx/scripts/office/unpack.py +5 -27
- package/skills/docx/scripts/office/validate.py +19 -14
- package/skills/docx/scripts/office/validators/base.py +48 -224
- package/skills/docx/scripts/office/validators/docx.py +44 -117
- package/skills/docx/scripts/office/validators/pptx.py +2 -42
- package/skills/docx/scripts/office/validators/redlining.py +3 -40
- package/skills/pdf/SKILL.md +22 -15
- package/skills/pdf/{FORMS.md → forms.md} +0 -14
- package/skills/pdf/scripts/check_bounding_boxes.py +0 -5
- package/skills/pdf/scripts/check_fillable_fields.py +0 -1
- package/skills/pdf/scripts/convert_pdf_to_images.py +0 -2
- package/skills/pdf/scripts/create_validation_image.py +0 -4
- package/skills/pdf/scripts/extract_form_field_info.py +1 -31
- package/skills/pdf/scripts/extract_form_structure.py +0 -9
- package/skills/pdf/scripts/fill_fillable_fields.py +0 -23
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +3 -38
- package/skills/pptx/SKILL.md +2 -29
- package/skills/pptx/editing.md +2 -2
- package/skills/pptx/pptxgenjs.md +53 -8
- package/skills/pptx/scripts/add_slide.py +0 -30
- package/skills/pptx/scripts/clean.py +0 -23
- package/skills/pptx/scripts/office/helpers/merge_runs.py +1 -33
- package/skills/pptx/scripts/office/helpers/simplify_redlines.py +0 -43
- package/skills/pptx/scripts/office/pack.py +0 -30
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -1499
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -1085
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -3081
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -287
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -1676
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -174
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -582
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -4439
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -570
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -116
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -42
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -50
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -49
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -33
- package/skills/pptx/scripts/office/soffice.py +0 -55
- package/skills/pptx/scripts/office/unpack.py +5 -27
- package/skills/pptx/scripts/office/validate.py +19 -14
- package/skills/pptx/scripts/office/validators/base.py +48 -224
- package/skills/pptx/scripts/office/validators/docx.py +44 -117
- package/skills/pptx/scripts/office/validators/pptx.py +2 -42
- package/skills/pptx/scripts/office/validators/redlining.py +3 -40
- package/skills/pptx/scripts/thumbnail.py +0 -31
- package/skills/xlsx/SKILL.md +3 -26
- package/skills/xlsx/scripts/office/helpers/merge_runs.py +1 -33
- package/skills/xlsx/scripts/office/helpers/simplify_redlines.py +0 -43
- package/skills/xlsx/scripts/office/pack.py +0 -30
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -1499
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -1085
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -3081
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -287
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -1676
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -174
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -582
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -4439
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -570
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -116
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -42
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -50
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -49
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -33
- package/skills/xlsx/scripts/office/soffice.py +0 -55
- package/skills/xlsx/scripts/office/unpack.py +5 -27
- package/skills/xlsx/scripts/office/validate.py +19 -14
- package/skills/xlsx/scripts/office/validators/base.py +48 -224
- package/skills/xlsx/scripts/office/validators/docx.py +44 -117
- package/skills/xlsx/scripts/office/validators/pptx.py +2 -42
- package/skills/xlsx/scripts/office/validators/redlining.py +3 -40
- package/skills/xlsx/scripts/recalc.py +2 -26
- package/skills/docx/scripts/__init__.py +0 -1
- package/skills/docx/scripts/office/helpers/__init__.py +0 -0
- package/skills/docx/scripts/office/validators/__init__.py +0 -15
- package/skills/pptx/scripts/__init__.py +0 -0
- package/skills/pptx/scripts/office/helpers/__init__.py +0 -0
- package/skills/pptx/scripts/office/validators/__init__.py +0 -15
- package/skills/xlsx/scripts/office/helpers/__init__.py +0 -0
- package/skills/xlsx/scripts/office/validators/__init__.py +0 -15
- /package/skills/pdf/{REFERENCE.md → reference.md} +0 -0
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sys
|
|
3
|
-
import os
|
|
4
3
|
|
|
5
4
|
from pypdf import PdfReader, PdfWriter
|
|
6
5
|
from pypdf.annotations import FreeText
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
# Fills a PDF by adding text annotations defined in `fields.json`. See FORMS.md.
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
def transform_from_image_coords(bbox, image_width, image_height, pdf_width, pdf_height):
|
|
13
|
-
"""Transform bounding box from image coordinates to PDF coordinates"""
|
|
14
|
-
# Image coordinates: origin at top-left, y increases downward
|
|
15
|
-
# PDF coordinates: origin at bottom-left, y increases upward
|
|
16
11
|
x_scale = pdf_width / image_width
|
|
17
12
|
y_scale = pdf_height / image_height
|
|
18
13
|
|
|
19
14
|
left = bbox[0] * x_scale
|
|
20
15
|
right = bbox[2] * x_scale
|
|
21
16
|
|
|
22
|
-
# Flip Y coordinates for PDF
|
|
23
17
|
top = pdf_height - (bbox[1] * y_scale)
|
|
24
18
|
bottom = pdf_height - (bbox[3] * y_scale)
|
|
25
19
|
|
|
@@ -27,67 +21,43 @@ def transform_from_image_coords(bbox, image_width, image_height, pdf_width, pdf_
|
|
|
27
21
|
|
|
28
22
|
|
|
29
23
|
def transform_from_pdf_coords(bbox, pdf_height):
|
|
30
|
-
"""Transform bounding box from pdfplumber coordinates to pypdf coordinates.
|
|
31
|
-
|
|
32
|
-
pdfplumber uses y=0 at top, y increases downward (like images).
|
|
33
|
-
pypdf FreeText expects y=0 at bottom, y increases upward.
|
|
34
|
-
Both use the same scale (PDF points), so only Y needs flipping.
|
|
35
|
-
"""
|
|
36
24
|
left = bbox[0]
|
|
37
25
|
right = bbox[2]
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
pypdf_top = pdf_height - bbox[1] # flip the "top" value
|
|
42
|
-
pypdf_bottom = pdf_height - bbox[3] # flip the "bottom" value
|
|
27
|
+
pypdf_top = pdf_height - bbox[1]
|
|
28
|
+
pypdf_bottom = pdf_height - bbox[3]
|
|
43
29
|
|
|
44
30
|
return left, pypdf_bottom, right, pypdf_top
|
|
45
31
|
|
|
46
32
|
|
|
47
33
|
def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path):
|
|
48
|
-
"""Fill the PDF form with data from fields.json"""
|
|
49
|
-
input_abs = os.path.abspath(input_pdf_path)
|
|
50
|
-
output_abs = os.path.abspath(output_pdf_path)
|
|
51
|
-
if input_abs == output_abs:
|
|
52
|
-
print("ERROR: Refusing to overwrite input PDF. Use a different output path.")
|
|
53
|
-
sys.exit(1)
|
|
54
34
|
|
|
55
|
-
# `fields.json` format described in FORMS.md.
|
|
56
35
|
with open(fields_json_path, "r") as f:
|
|
57
36
|
fields_data = json.load(f)
|
|
58
37
|
|
|
59
|
-
# Open the PDF
|
|
60
38
|
reader = PdfReader(input_pdf_path)
|
|
61
39
|
writer = PdfWriter()
|
|
62
40
|
|
|
63
|
-
# Copy all pages to writer
|
|
64
41
|
writer.append(reader)
|
|
65
42
|
|
|
66
|
-
# Get PDF dimensions for each page
|
|
67
43
|
pdf_dimensions = {}
|
|
68
44
|
for i, page in enumerate(reader.pages):
|
|
69
45
|
mediabox = page.mediabox
|
|
70
46
|
pdf_dimensions[i + 1] = [mediabox.width, mediabox.height]
|
|
71
47
|
|
|
72
|
-
# Process each form field
|
|
73
48
|
annotations = []
|
|
74
49
|
for field in fields_data["form_fields"]:
|
|
75
50
|
page_num = field["page_number"]
|
|
76
51
|
|
|
77
|
-
# Get page dimensions and transform coordinates.
|
|
78
52
|
page_info = next(p for p in fields_data["pages"] if p["page_number"] == page_num)
|
|
79
53
|
pdf_width, pdf_height = pdf_dimensions[page_num]
|
|
80
54
|
|
|
81
|
-
# Detect coordinate system: pdf_width/pdf_height = PDF coords, image_width/image_height = image coords
|
|
82
55
|
if "pdf_width" in page_info:
|
|
83
|
-
# PDF coordinates from structure extraction (pdfplumber style)
|
|
84
|
-
# Only need Y-flip, no scaling
|
|
85
56
|
transformed_entry_box = transform_from_pdf_coords(
|
|
86
57
|
field["entry_bounding_box"],
|
|
87
58
|
float(pdf_height)
|
|
88
59
|
)
|
|
89
60
|
else:
|
|
90
|
-
# Image coordinates - need scaling and Y-flip
|
|
91
61
|
image_width = page_info["image_width"]
|
|
92
62
|
image_height = page_info["image_height"]
|
|
93
63
|
transformed_entry_box = transform_from_image_coords(
|
|
@@ -96,7 +66,6 @@ def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path):
|
|
|
96
66
|
float(pdf_width), float(pdf_height)
|
|
97
67
|
)
|
|
98
68
|
|
|
99
|
-
# Skip empty fields
|
|
100
69
|
if "entry_text" not in field or "text" not in field["entry_text"]:
|
|
101
70
|
continue
|
|
102
71
|
entry_text = field["entry_text"]
|
|
@@ -108,8 +77,6 @@ def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path):
|
|
|
108
77
|
font_size = str(entry_text.get("font_size", 14)) + "pt"
|
|
109
78
|
font_color = entry_text.get("font_color", "000000")
|
|
110
79
|
|
|
111
|
-
# Font size/color seems to not work reliably across viewers:
|
|
112
|
-
# https://github.com/py-pdf/pypdf/issues/2084
|
|
113
80
|
annotation = FreeText(
|
|
114
81
|
text=text,
|
|
115
82
|
rect=transformed_entry_box,
|
|
@@ -120,10 +87,8 @@ def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path):
|
|
|
120
87
|
background_color=None,
|
|
121
88
|
)
|
|
122
89
|
annotations.append(annotation)
|
|
123
|
-
# page_number is 0-based for pypdf
|
|
124
90
|
writer.add_annotation(page_number=page_num - 1, annotation=annotation)
|
|
125
91
|
|
|
126
|
-
# Save the filled PDF
|
|
127
92
|
with open(output_pdf_path, "wb") as output:
|
|
128
93
|
writer.write(output)
|
|
129
94
|
|
|
@@ -139,4 +104,4 @@ if __name__ == "__main__":
|
|
|
139
104
|
fields_json = sys.argv[2]
|
|
140
105
|
output_pdf = sys.argv[3]
|
|
141
106
|
|
|
142
|
-
fill_pdf_form(input_pdf, fields_json, output_pdf)
|
|
107
|
+
fill_pdf_form(input_pdf, fields_json, output_pdf)
|
package/skills/pptx/SKILL.md
CHANGED
|
@@ -1,38 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pptx
|
|
3
|
-
description: "
|
|
3
|
+
description: "Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill."
|
|
4
4
|
license: Proprietary. LICENSE.txt has complete terms
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# PPTX Skill
|
|
8
8
|
|
|
9
|
-
## IMPORTANT: Save to Desktop
|
|
10
|
-
|
|
11
|
-
**Always save created `.pptx` files to `~/Desktop/`** (e.g. `~/Desktop/presentation.pptx`). Never save to the agent workspace or hidden directories — the user needs easy access to the file.
|
|
12
|
-
|
|
13
|
-
## CRITICAL: Integration Priority
|
|
14
|
-
|
|
15
|
-
### 1. `lemon-slides` CLI (For Google Slides)
|
|
16
|
-
If user wants a Google Slides presentation (shareable, collaborative), use `lemon-slides`:
|
|
17
|
-
- `lemon-slides create "Title"` - Create a new presentation
|
|
18
|
-
- `lemon-slides read <id>` - Read a presentation
|
|
19
|
-
|
|
20
|
-
### 2. Local PPTX (Recommended for Quality)
|
|
21
|
-
For best results, create presentations locally using pptxgenjs:
|
|
22
|
-
- Full control over every element
|
|
23
|
-
- Professional, polished output
|
|
24
|
-
- Files user can open in PowerPoint, Keynote, or upload to Google Slides
|
|
25
|
-
|
|
26
|
-
### 3. Browser (LAST RESORT)
|
|
27
|
-
Only if `lemon-slides` CLI fails AND user explicitly requests Google Slides in browser.
|
|
28
|
-
|
|
29
|
-
## Workflow Summary (Local Creation)
|
|
30
|
-
|
|
31
|
-
1. **Create slides** using pptxgenjs (JavaScript)
|
|
32
|
-
2. **Generate `.pptx` file** locally
|
|
33
|
-
3. **QA the output** using visual inspection
|
|
34
|
-
4. **Deliver** the file to the user
|
|
35
|
-
|
|
36
9
|
## Quick Reference
|
|
37
10
|
|
|
38
11
|
| Task | Guide |
|
|
@@ -161,7 +134,7 @@ Choose colors that match your topic — don't default to generic blue. Use these
|
|
|
161
134
|
- **Don't create text-only slides** — add images, icons, charts, or visual elements; avoid plain title + bullets
|
|
162
135
|
- **Don't forget text box padding** — when aligning lines or shapes with text edges, set `margin: 0` on the text box or offset the shape to account for padding
|
|
163
136
|
- **Don't use low-contrast elements** — icons AND text need strong contrast against the background; avoid light text on light backgrounds or dark text on dark backgrounds
|
|
164
|
-
- **NEVER use
|
|
137
|
+
- **NEVER use accent lines under titles** — these are a hallmark of AI-generated slides; use whitespace or background color instead
|
|
165
138
|
|
|
166
139
|
---
|
|
167
140
|
|
package/skills/pptx/editing.md
CHANGED
|
@@ -39,7 +39,7 @@ When using an existing presentation as a template:
|
|
|
39
39
|
|
|
40
40
|
6. **Clean**: `python scripts/clean.py unpacked/`
|
|
41
41
|
|
|
42
|
-
7. **Pack**: `python scripts/office/pack.py unpacked/
|
|
42
|
+
7. **Pack**: `python scripts/office/pack.py unpacked/ output.pptx --original template.pptx`
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
@@ -81,7 +81,7 @@ Removes slides not in `<p:sldIdLst>`, unreferenced media, orphaned rels.
|
|
|
81
81
|
### pack.py
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
python scripts/office/pack.py unpacked/
|
|
84
|
+
python scripts/office/pack.py unpacked/ output.pptx --original input.pptx
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
Validates, repairs, condenses XML, re-encodes smart quotes.
|
package/skills/pptx/pptxgenjs.md
CHANGED
|
@@ -13,9 +13,7 @@ pres.title = 'Presentation Title';
|
|
|
13
13
|
let slide = pres.addSlide();
|
|
14
14
|
slide.addText("Hello World!", { x: 0.5, y: 0.5, fontSize: 36, color: "363636" });
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
const path = require('path');
|
|
18
|
-
pres.writeFile({ fileName: path.join(os.homedir(), "Desktop", "Presentation.pptx") });
|
|
16
|
+
pres.writeFile({ fileName: "Presentation.pptx" });
|
|
19
17
|
```
|
|
20
18
|
|
|
21
19
|
## Layout Dimensions
|
|
@@ -37,6 +35,9 @@ slide.addText("Simple Text", {
|
|
|
37
35
|
color: "363636", bold: true, align: "center", valign: "middle"
|
|
38
36
|
});
|
|
39
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
|
+
|
|
40
41
|
// Rich text arrays
|
|
41
42
|
slide.addText([
|
|
42
43
|
{ text: "Bold ", options: { bold: true } },
|
|
@@ -100,8 +101,35 @@ slide.addShape(pres.shapes.RECTANGLE, {
|
|
|
100
101
|
x: 1, y: 1, w: 3, h: 2,
|
|
101
102
|
fill: { color: "0088CC", transparency: 50 }
|
|
102
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
|
+
});
|
|
103
118
|
```
|
|
104
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
|
+
|
|
105
133
|
**Note**: Gradient fills are not natively supported. Use a gradient image as a background instead.
|
|
106
134
|
|
|
107
135
|
---
|
|
@@ -345,15 +373,32 @@ titleSlide.addText("My Title", { placeholder: "title" });
|
|
|
345
373
|
color: "#FF0000" // ❌ WRONG
|
|
346
374
|
```
|
|
347
375
|
|
|
348
|
-
2. **
|
|
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)
|
|
349
383
|
|
|
350
|
-
|
|
384
|
+
4. **Use `breakLine: true`** between array items or text runs together
|
|
351
385
|
|
|
352
|
-
|
|
386
|
+
5. **Avoid `lineSpacing` with bullets** - causes excessive gaps; use `paraSpaceAfter` instead
|
|
353
387
|
|
|
354
|
-
|
|
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
|
+
```
|
|
355
400
|
|
|
356
|
-
|
|
401
|
+
8. **Don't use `ROUNDED_RECTANGLE` with accent borders** - rectangular overlay bars won't cover rounded corners. Use `RECTANGLE` instead.
|
|
357
402
|
```javascript
|
|
358
403
|
// ❌ WRONG: Accent bar doesn't cover rounded corners
|
|
359
404
|
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } });
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""Add a new slide to an unpacked PPTX directory.
|
|
3
2
|
|
|
4
3
|
Usage: python add_slide.py <unpacked_dir> <source>
|
|
@@ -26,14 +25,12 @@ from pathlib import Path
|
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
def get_next_slide_number(slides_dir: Path) -> int:
|
|
29
|
-
"""Find the next available slide number."""
|
|
30
28
|
existing = [int(m.group(1)) for f in slides_dir.glob("slide*.xml")
|
|
31
29
|
if (m := re.match(r"slide(\d+)\.xml", f.name))]
|
|
32
30
|
return max(existing) + 1 if existing else 1
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
def create_slide_from_layout(unpacked_dir: Path, layout_file: str) -> None:
|
|
36
|
-
"""Create a new slide from a layout template."""
|
|
37
34
|
slides_dir = unpacked_dir / "ppt" / "slides"
|
|
38
35
|
rels_dir = slides_dir / "_rels"
|
|
39
36
|
layouts_dir = unpacked_dir / "ppt" / "slideLayouts"
|
|
@@ -43,13 +40,11 @@ def create_slide_from_layout(unpacked_dir: Path, layout_file: str) -> None:
|
|
|
43
40
|
print(f"Error: {layout_path} not found", file=sys.stderr)
|
|
44
41
|
sys.exit(1)
|
|
45
42
|
|
|
46
|
-
# Auto-select destination name
|
|
47
43
|
next_num = get_next_slide_number(slides_dir)
|
|
48
44
|
dest = f"slide{next_num}.xml"
|
|
49
45
|
dest_slide = slides_dir / dest
|
|
50
46
|
dest_rels = rels_dir / f"{dest}.rels"
|
|
51
47
|
|
|
52
|
-
# 1. Create minimal slide XML that references the layout
|
|
53
48
|
slide_xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
54
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">
|
|
55
50
|
<p:cSld>
|
|
@@ -75,7 +70,6 @@ def create_slide_from_layout(unpacked_dir: Path, layout_file: str) -> None:
|
|
|
75
70
|
</p:sld>'''
|
|
76
71
|
dest_slide.write_text(slide_xml, encoding="utf-8")
|
|
77
72
|
|
|
78
|
-
# 2. Create relationships file pointing to the layout
|
|
79
73
|
rels_dir.mkdir(exist_ok=True)
|
|
80
74
|
rels_xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
81
75
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
@@ -83,33 +77,26 @@ def create_slide_from_layout(unpacked_dir: Path, layout_file: str) -> None:
|
|
|
83
77
|
</Relationships>'''
|
|
84
78
|
dest_rels.write_text(rels_xml, encoding="utf-8")
|
|
85
79
|
|
|
86
|
-
# 3. Add to [Content_Types].xml
|
|
87
80
|
_add_to_content_types(unpacked_dir, dest)
|
|
88
81
|
|
|
89
|
-
# 4. Add relationship to presentation.xml.rels and get rId
|
|
90
82
|
rid = _add_to_presentation_rels(unpacked_dir, dest)
|
|
91
83
|
|
|
92
|
-
# 5. Get next slide ID
|
|
93
84
|
next_slide_id = _get_next_slide_id(unpacked_dir)
|
|
94
85
|
|
|
95
|
-
# Output
|
|
96
86
|
print(f"Created {dest} from {layout_file}")
|
|
97
87
|
print(f'Add to presentation.xml <p:sldIdLst>: <p:sldId id="{next_slide_id}" r:id="{rid}"/>')
|
|
98
88
|
|
|
99
89
|
|
|
100
90
|
def duplicate_slide(unpacked_dir: Path, source: str) -> None:
|
|
101
|
-
"""Duplicate a slide and update all references."""
|
|
102
91
|
slides_dir = unpacked_dir / "ppt" / "slides"
|
|
103
92
|
rels_dir = slides_dir / "_rels"
|
|
104
93
|
|
|
105
94
|
source_slide = slides_dir / source
|
|
106
95
|
|
|
107
|
-
# Validate source exists
|
|
108
96
|
if not source_slide.exists():
|
|
109
97
|
print(f"Error: {source_slide} not found", file=sys.stderr)
|
|
110
98
|
sys.exit(1)
|
|
111
99
|
|
|
112
|
-
# Auto-select destination name
|
|
113
100
|
next_num = get_next_slide_number(slides_dir)
|
|
114
101
|
dest = f"slide{next_num}.xml"
|
|
115
102
|
dest_slide = slides_dir / dest
|
|
@@ -117,14 +104,11 @@ def duplicate_slide(unpacked_dir: Path, source: str) -> None:
|
|
|
117
104
|
source_rels = rels_dir / f"{source}.rels"
|
|
118
105
|
dest_rels = rels_dir / f"{dest}.rels"
|
|
119
106
|
|
|
120
|
-
# 1. Copy slide XML
|
|
121
107
|
shutil.copy2(source_slide, dest_slide)
|
|
122
108
|
|
|
123
|
-
# 2. Copy relationships file (if exists)
|
|
124
109
|
if source_rels.exists():
|
|
125
110
|
shutil.copy2(source_rels, dest_rels)
|
|
126
111
|
|
|
127
|
-
# 3. Remove notes references from new rels file
|
|
128
112
|
rels_content = dest_rels.read_text(encoding="utf-8")
|
|
129
113
|
rels_content = re.sub(
|
|
130
114
|
r'\s*<Relationship[^>]*Type="[^"]*notesSlide"[^>]*/>\s*',
|
|
@@ -133,22 +117,17 @@ def duplicate_slide(unpacked_dir: Path, source: str) -> None:
|
|
|
133
117
|
)
|
|
134
118
|
dest_rels.write_text(rels_content, encoding="utf-8")
|
|
135
119
|
|
|
136
|
-
# 4. Add to [Content_Types].xml
|
|
137
120
|
_add_to_content_types(unpacked_dir, dest)
|
|
138
121
|
|
|
139
|
-
# 5. Add relationship to presentation.xml.rels
|
|
140
122
|
rid = _add_to_presentation_rels(unpacked_dir, dest)
|
|
141
123
|
|
|
142
|
-
# 6. Get next slide ID
|
|
143
124
|
next_slide_id = _get_next_slide_id(unpacked_dir)
|
|
144
125
|
|
|
145
|
-
# Output
|
|
146
126
|
print(f"Created {dest} from {source}")
|
|
147
127
|
print(f'Add to presentation.xml <p:sldIdLst>: <p:sldId id="{next_slide_id}" r:id="{rid}"/>')
|
|
148
128
|
|
|
149
129
|
|
|
150
130
|
def _add_to_content_types(unpacked_dir: Path, dest: str) -> None:
|
|
151
|
-
"""Add new slide to [Content_Types].xml."""
|
|
152
131
|
content_types_path = unpacked_dir / "[Content_Types].xml"
|
|
153
132
|
content_types = content_types_path.read_text(encoding="utf-8")
|
|
154
133
|
|
|
@@ -160,7 +139,6 @@ def _add_to_content_types(unpacked_dir: Path, dest: str) -> None:
|
|
|
160
139
|
|
|
161
140
|
|
|
162
141
|
def _add_to_presentation_rels(unpacked_dir: Path, dest: str) -> str:
|
|
163
|
-
"""Add relationship to presentation.xml.rels. Returns the new rId."""
|
|
164
142
|
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
|
165
143
|
pres_rels = pres_rels_path.read_text(encoding="utf-8")
|
|
166
144
|
|
|
@@ -178,7 +156,6 @@ def _add_to_presentation_rels(unpacked_dir: Path, dest: str) -> str:
|
|
|
178
156
|
|
|
179
157
|
|
|
180
158
|
def _get_next_slide_id(unpacked_dir: Path) -> int:
|
|
181
|
-
"""Get the next available slide ID for presentation.xml."""
|
|
182
159
|
pres_path = unpacked_dir / "ppt" / "presentation.xml"
|
|
183
160
|
pres_content = pres_path.read_text(encoding="utf-8")
|
|
184
161
|
slide_ids = [int(m) for m in re.findall(r'<p:sldId[^>]*id="(\d+)"', pres_content)]
|
|
@@ -186,16 +163,9 @@ def _get_next_slide_id(unpacked_dir: Path) -> int:
|
|
|
186
163
|
|
|
187
164
|
|
|
188
165
|
def parse_source(source: str) -> tuple[str, str | None]:
|
|
189
|
-
"""Parse source argument to determine if it's a slide or layout.
|
|
190
|
-
|
|
191
|
-
Returns:
|
|
192
|
-
("slide", None) if source is a slide file like "slide2.xml"
|
|
193
|
-
("layout", filename) if source is a layout like "slideLayout2.xml"
|
|
194
|
-
"""
|
|
195
166
|
if source.startswith("slideLayout") and source.endswith(".xml"):
|
|
196
167
|
return ("layout", source)
|
|
197
168
|
|
|
198
|
-
# Otherwise treat as a slide file
|
|
199
169
|
return ("slide", None)
|
|
200
170
|
|
|
201
171
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""Remove unreferenced files from an unpacked PPTX directory.
|
|
3
2
|
|
|
4
3
|
Usage: python clean.py <unpacked_dir>
|
|
@@ -26,14 +25,12 @@ import re
|
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
def get_slides_in_sldidlst(unpacked_dir: Path) -> set[str]:
|
|
29
|
-
"""Get slide filenames referenced in presentation.xml sldIdLst."""
|
|
30
28
|
pres_path = unpacked_dir / "ppt" / "presentation.xml"
|
|
31
29
|
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
|
32
30
|
|
|
33
31
|
if not pres_path.exists() or not pres_rels_path.exists():
|
|
34
32
|
return set()
|
|
35
33
|
|
|
36
|
-
# Build rId -> slide filename mapping from presentation.xml.rels
|
|
37
34
|
rels_dom = defusedxml.minidom.parse(str(pres_rels_path))
|
|
38
35
|
rid_to_slide = {}
|
|
39
36
|
for rel in rels_dom.getElementsByTagName("Relationship"):
|
|
@@ -43,16 +40,13 @@ def get_slides_in_sldidlst(unpacked_dir: Path) -> set[str]:
|
|
|
43
40
|
if "slide" in rel_type and target.startswith("slides/"):
|
|
44
41
|
rid_to_slide[rid] = target.replace("slides/", "")
|
|
45
42
|
|
|
46
|
-
# Get rIds from sldIdLst in presentation.xml
|
|
47
43
|
pres_content = pres_path.read_text(encoding="utf-8")
|
|
48
44
|
referenced_rids = set(re.findall(r'<p:sldId[^>]*r:id="([^"]+)"', pres_content))
|
|
49
45
|
|
|
50
|
-
# Return slide filenames that are in sldIdLst
|
|
51
46
|
return {rid_to_slide[rid] for rid in referenced_rids if rid in rid_to_slide}
|
|
52
47
|
|
|
53
48
|
|
|
54
49
|
def remove_orphaned_slides(unpacked_dir: Path) -> list[str]:
|
|
55
|
-
"""Remove slides not referenced in sldIdLst."""
|
|
56
50
|
slides_dir = unpacked_dir / "ppt" / "slides"
|
|
57
51
|
slides_rels_dir = slides_dir / "_rels"
|
|
58
52
|
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
|
@@ -63,21 +57,17 @@ def remove_orphaned_slides(unpacked_dir: Path) -> list[str]:
|
|
|
63
57
|
referenced_slides = get_slides_in_sldidlst(unpacked_dir)
|
|
64
58
|
removed = []
|
|
65
59
|
|
|
66
|
-
# Find and remove orphaned slide files
|
|
67
60
|
for slide_file in slides_dir.glob("slide*.xml"):
|
|
68
61
|
if slide_file.name not in referenced_slides:
|
|
69
|
-
# Remove slide file
|
|
70
62
|
rel_path = slide_file.relative_to(unpacked_dir)
|
|
71
63
|
slide_file.unlink()
|
|
72
64
|
removed.append(str(rel_path))
|
|
73
65
|
|
|
74
|
-
# Remove slide rels file
|
|
75
66
|
rels_file = slides_rels_dir / f"{slide_file.name}.rels"
|
|
76
67
|
if rels_file.exists():
|
|
77
68
|
rels_file.unlink()
|
|
78
69
|
removed.append(str(rels_file.relative_to(unpacked_dir)))
|
|
79
70
|
|
|
80
|
-
# Remove relationships from presentation.xml.rels for deleted slides
|
|
81
71
|
if removed and pres_rels_path.exists():
|
|
82
72
|
rels_dom = defusedxml.minidom.parse(str(pres_rels_path))
|
|
83
73
|
changed = False
|
|
@@ -99,7 +89,6 @@ def remove_orphaned_slides(unpacked_dir: Path) -> list[str]:
|
|
|
99
89
|
|
|
100
90
|
|
|
101
91
|
def remove_trash_directory(unpacked_dir: Path) -> list[str]:
|
|
102
|
-
"""Remove [trash] directory if it exists."""
|
|
103
92
|
trash_dir = unpacked_dir / "[trash]"
|
|
104
93
|
removed = []
|
|
105
94
|
|
|
@@ -115,7 +104,6 @@ def remove_trash_directory(unpacked_dir: Path) -> list[str]:
|
|
|
115
104
|
|
|
116
105
|
|
|
117
106
|
def get_slide_referenced_files(unpacked_dir: Path) -> set:
|
|
118
|
-
"""Get files referenced directly from slides."""
|
|
119
107
|
referenced = set()
|
|
120
108
|
slides_rels_dir = unpacked_dir / "ppt" / "slides" / "_rels"
|
|
121
109
|
|
|
@@ -138,7 +126,6 @@ def get_slide_referenced_files(unpacked_dir: Path) -> set:
|
|
|
138
126
|
|
|
139
127
|
|
|
140
128
|
def remove_orphaned_rels_files(unpacked_dir: Path) -> list[str]:
|
|
141
|
-
"""Remove .rels files for unreferenced resources."""
|
|
142
129
|
resource_dirs = ["charts", "diagrams", "drawings"]
|
|
143
130
|
removed = []
|
|
144
131
|
slide_referenced = get_slide_referenced_files(unpacked_dir)
|
|
@@ -164,7 +151,6 @@ def remove_orphaned_rels_files(unpacked_dir: Path) -> list[str]:
|
|
|
164
151
|
|
|
165
152
|
|
|
166
153
|
def get_referenced_files(unpacked_dir: Path) -> set:
|
|
167
|
-
"""Get all files referenced in .rels files."""
|
|
168
154
|
referenced = set()
|
|
169
155
|
|
|
170
156
|
for rels_file in unpacked_dir.rglob("*.rels"):
|
|
@@ -183,7 +169,6 @@ def get_referenced_files(unpacked_dir: Path) -> set:
|
|
|
183
169
|
|
|
184
170
|
|
|
185
171
|
def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]:
|
|
186
|
-
"""Remove files not in the referenced set."""
|
|
187
172
|
resource_dirs = ["media", "embeddings", "charts", "diagrams", "tags", "drawings", "ink"]
|
|
188
173
|
removed = []
|
|
189
174
|
|
|
@@ -200,7 +185,6 @@ def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]:
|
|
|
200
185
|
file_path.unlink()
|
|
201
186
|
removed.append(str(rel_path))
|
|
202
187
|
|
|
203
|
-
# Clean up unreferenced theme files
|
|
204
188
|
theme_dir = unpacked_dir / "ppt" / "theme"
|
|
205
189
|
if theme_dir.exists():
|
|
206
190
|
for file_path in theme_dir.glob("theme*.xml"):
|
|
@@ -208,13 +192,11 @@ def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]:
|
|
|
208
192
|
if rel_path not in referenced:
|
|
209
193
|
file_path.unlink()
|
|
210
194
|
removed.append(str(rel_path))
|
|
211
|
-
# Also remove corresponding .rels
|
|
212
195
|
theme_rels = theme_dir / "_rels" / f"{file_path.name}.rels"
|
|
213
196
|
if theme_rels.exists():
|
|
214
197
|
theme_rels.unlink()
|
|
215
198
|
removed.append(str(theme_rels.relative_to(unpacked_dir)))
|
|
216
199
|
|
|
217
|
-
# Remove orphaned notes slides
|
|
218
200
|
notes_dir = unpacked_dir / "ppt" / "notesSlides"
|
|
219
201
|
if notes_dir.exists():
|
|
220
202
|
for file_path in notes_dir.glob("*.xml"):
|
|
@@ -237,7 +219,6 @@ def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]:
|
|
|
237
219
|
|
|
238
220
|
|
|
239
221
|
def update_content_types(unpacked_dir: Path, removed_files: list[str]) -> None:
|
|
240
|
-
"""Remove Content-Type overrides for deleted files."""
|
|
241
222
|
ct_path = unpacked_dir / "[Content_Types].xml"
|
|
242
223
|
if not ct_path.exists():
|
|
243
224
|
return
|
|
@@ -258,18 +239,14 @@ def update_content_types(unpacked_dir: Path, removed_files: list[str]) -> None:
|
|
|
258
239
|
|
|
259
240
|
|
|
260
241
|
def clean_unused_files(unpacked_dir: Path) -> list[str]:
|
|
261
|
-
"""Remove all unreferenced files from the unpacked directory."""
|
|
262
242
|
all_removed = []
|
|
263
243
|
|
|
264
|
-
# Remove orphaned slides first (not in sldIdLst)
|
|
265
244
|
slides_removed = remove_orphaned_slides(unpacked_dir)
|
|
266
245
|
all_removed.extend(slides_removed)
|
|
267
246
|
|
|
268
|
-
# Remove [trash] directory
|
|
269
247
|
trash_removed = remove_trash_directory(unpacked_dir)
|
|
270
248
|
all_removed.extend(trash_removed)
|
|
271
249
|
|
|
272
|
-
# Keep cleaning until nothing more is removed
|
|
273
250
|
while True:
|
|
274
251
|
removed_rels = remove_orphaned_rels_files(unpacked_dir)
|
|
275
252
|
referenced = get_referenced_files(unpacked_dir)
|