excalidraw-convert 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lyndon Liang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: excalidraw-convert
3
+ Version: 0.1.0
4
+ Summary: Convert Excalidraw diagrams to PowerPoint or Lucid Package Format
5
+ Author: Lyndon Liang
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/lliang17/excalidraw-convert
8
+ Project-URL: Issues, https://github.com/lliang17/excalidraw-convert/issues
9
+ Keywords: excalidraw,powerpoint,pptx,diagram,convert
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Multimedia :: Graphics :: Presentation
19
+ Classifier: Topic :: Office/Business :: Office Suites
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: python-pptx>=0.6.21
25
+ Requires-Dist: requests>=2.28.0
26
+ Requires-Dist: cryptography>=41.0.0
27
+ Requires-Dist: click>=8.0.0
28
+ Requires-Dist: Pillow>=10.0.0
29
+ Provides-Extra: svg
30
+ Requires-Dist: cairosvg>=2.7.0; extra == "svg"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # excalidraw-convert
37
+
38
+ Convert Excalidraw diagrams to **editable PowerPoint vector objects** or **Lucid Package Format** (`.lucid`).
39
+
40
+ ---
41
+
42
+ ## Features
43
+
44
+ ### PowerPoint output
45
+
46
+ Each shape, arrow, and text box becomes a native PowerPoint object you can move, resize, recolour, and animate individually — not a flat image.
47
+
48
+ | Excalidraw element | PowerPoint output |
49
+ |---|---|
50
+ | Rectangle (with/without roundness) | Auto-shape (Rectangle / Rounded Rectangle) |
51
+ | Ellipse | Auto-shape (Oval) |
52
+ | Diamond | Auto-shape (Diamond) |
53
+ | Arrow (2-point) | Straight Connector with arrowheads |
54
+ | Line / Arrow (multi-point) | Freeform path |
55
+ | Freedraw | Freeform path (approximate) |
56
+ | Text (standalone) | Text Box |
57
+ | Text (bound to shape) | Text inside the parent shape |
58
+ | Image (PNG/JPEG/WEBP) | Embedded picture |
59
+ | Image (SVG) | PNG fallback _(requires `cairosvg`)_ |
60
+ | Frame | Separate slide |
61
+ | No frames | Single slide (whole canvas) |
62
+
63
+ ### Lucid output
64
+
65
+ Produces a `.lucid` file (ZIP archive in [Lucid Package Format](https://lucid.readme.io/docs/overview-si)) that can be opened directly in LucidChart or LucidSpark.
66
+
67
+ | Excalidraw element | Lucid output |
68
+ |---|---|
69
+ | Rectangle (with/without roundness) | `rectangle` shape |
70
+ | Ellipse | `circle` shape |
71
+ | Diamond | `diamond` shape |
72
+ | Arrow / Line (2-point) | `elbow` line connector |
73
+ | Arrow / Line (multi-point) | `straight` line with intermediate joints |
74
+ | Freedraw | `flexiblePolygon` (downsampled to ≤100 vertices) |
75
+ | Text (standalone) | `text` shape |
76
+ | Text (bound to shape) | Text property on the parent shape |
77
+ | Image | `image` shape with file embedded in the archive |
78
+ | Frame | Separate page |
79
+ | No frames | Single page (whole canvas) |
80
+
81
+ ### Styling carried over (both formats)
82
+
83
+ Fill colour, stroke colour, stroke width, dashed/dotted strokes, rounded corners, arrowhead types, opacity, background colour, multi-line text, font family, font size, and text alignment.
84
+
85
+ ---
86
+
87
+ ## Installation
88
+
89
+ ```bash
90
+ pip install excalidraw-convert
91
+ ```
92
+
93
+ For SVG image support in PowerPoint output (optional):
94
+ ```bash
95
+ pip install "excalidraw-convert[svg]"
96
+ ```
97
+
98
+ To install from source:
99
+ ```bash
100
+ pip install -e ".[dev]"
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Usage
106
+
107
+ ### Command line
108
+
109
+ ```bash
110
+ # Convert to PowerPoint
111
+ excalidraw-convert convert "https://excalidraw.com/#json=<id>,<key>" -o output.pptx
112
+
113
+ # Convert to Lucid Package Format
114
+ excalidraw-convert convert-lucid "https://excalidraw.com/#json=<id>,<key>" -o output.lucid
115
+
116
+ # From a direct JSON / .excalidraw file URL
117
+ excalidraw-convert convert "https://raw.githubusercontent.com/you/repo/main/diagram.excalidraw" -o output.pptx
118
+
119
+ # Append slides to an existing presentation
120
+ excalidraw-convert convert "https://..." -o existing.pptx --embed
121
+
122
+ # Inspect a scene before converting
123
+ excalidraw-convert inspect "https://..."
124
+ ```
125
+
126
+ Also callable as a module:
127
+ ```bash
128
+ python -m excalidraw_convert convert "https://..." -o output.pptx
129
+ python -m excalidraw_convert convert-lucid "https://..." -o output.lucid
130
+ ```
131
+
132
+ ### Python API
133
+
134
+ ```python
135
+ from excalidraw_convert import fetch_scene, convert, convert_to_lucid
136
+
137
+ scene = fetch_scene("https://excalidraw.com/#json=<id>,<key>")
138
+
139
+ # PowerPoint
140
+ convert(scene, "output.pptx")
141
+
142
+ # Lucid Package Format
143
+ convert_to_lucid(scene, "output.lucid")
144
+
145
+ # Append to an existing PowerPoint file
146
+ convert(scene, "existing.pptx", embed_path="existing.pptx")
147
+
148
+ # Custom slide dimensions (inches, new files only)
149
+ convert(scene, "output.pptx", slide_width=10, slide_height=7.5)
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Supported URL formats
155
+
156
+ | Format | Example |
157
+ |---|---|
158
+ | Excalidraw share link | `https://excalidraw.com/#json=abc123,base64key` |
159
+ | Raw `.excalidraw` file URL | `https://raw.githubusercontent.com/.../diagram.excalidraw` |
160
+ | Any URL returning JSON | `https://your-server.com/scene.json` |
161
+
162
+ > **Note on Excalidraw share links:** the scene is end-to-end encrypted in the browser. This tool replicates the AES-GCM decryption that the Excalidraw web app performs client-side. If decryption fails for a link, try exporting the scene as a `.excalidraw` file and hosting it somewhere accessible.
163
+
164
+ ---
165
+
166
+ ## Limitations / known gaps
167
+
168
+ ### Both formats
169
+ - **Roughness / hand-drawn style**: Excalidraw's sketchy aesthetic is not reproduced — shapes are rendered as clean geometric shapes.
170
+ - **Hachure fill**: approximated as a solid fill.
171
+ - **Curved / elbow arrows**: rendered as straight connectors or straight segments.
172
+ - **`freedraw` pressures**: variable stroke-width pressure data is ignored; a fixed width is used.
173
+ - **Grouped elements**: group membership is preserved in rendering order but not as native groups.
174
+ - **Embeddable / iframe elements**: skipped with a warning.
175
+ - **Fonts**: Excalidraw's *Virgil* hand-drawn font is mapped to *Caveat* (install it for best results; falls back to the system default).
176
+
177
+ ### PowerPoint only
178
+ - `freedraw` elements are rendered as freeform polygon paths (not true brush strokes).
179
+
180
+ ### Lucid only
181
+ - `freedraw` elements are approximated as `flexiblePolygon` shapes with up to 100 sampled vertices.
182
+ - Arrow connection points snap to the nearest edge of the target shape.
183
+
184
+ ---
185
+
186
+ ## Development
187
+
188
+ ```bash
189
+ pip install -e ".[dev]"
190
+ pytest tests/ -v
191
+ ```
@@ -0,0 +1,156 @@
1
+ # excalidraw-convert
2
+
3
+ Convert Excalidraw diagrams to **editable PowerPoint vector objects** or **Lucid Package Format** (`.lucid`).
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ ### PowerPoint output
10
+
11
+ Each shape, arrow, and text box becomes a native PowerPoint object you can move, resize, recolour, and animate individually — not a flat image.
12
+
13
+ | Excalidraw element | PowerPoint output |
14
+ |---|---|
15
+ | Rectangle (with/without roundness) | Auto-shape (Rectangle / Rounded Rectangle) |
16
+ | Ellipse | Auto-shape (Oval) |
17
+ | Diamond | Auto-shape (Diamond) |
18
+ | Arrow (2-point) | Straight Connector with arrowheads |
19
+ | Line / Arrow (multi-point) | Freeform path |
20
+ | Freedraw | Freeform path (approximate) |
21
+ | Text (standalone) | Text Box |
22
+ | Text (bound to shape) | Text inside the parent shape |
23
+ | Image (PNG/JPEG/WEBP) | Embedded picture |
24
+ | Image (SVG) | PNG fallback _(requires `cairosvg`)_ |
25
+ | Frame | Separate slide |
26
+ | No frames | Single slide (whole canvas) |
27
+
28
+ ### Lucid output
29
+
30
+ Produces a `.lucid` file (ZIP archive in [Lucid Package Format](https://lucid.readme.io/docs/overview-si)) that can be opened directly in LucidChart or LucidSpark.
31
+
32
+ | Excalidraw element | Lucid output |
33
+ |---|---|
34
+ | Rectangle (with/without roundness) | `rectangle` shape |
35
+ | Ellipse | `circle` shape |
36
+ | Diamond | `diamond` shape |
37
+ | Arrow / Line (2-point) | `elbow` line connector |
38
+ | Arrow / Line (multi-point) | `straight` line with intermediate joints |
39
+ | Freedraw | `flexiblePolygon` (downsampled to ≤100 vertices) |
40
+ | Text (standalone) | `text` shape |
41
+ | Text (bound to shape) | Text property on the parent shape |
42
+ | Image | `image` shape with file embedded in the archive |
43
+ | Frame | Separate page |
44
+ | No frames | Single page (whole canvas) |
45
+
46
+ ### Styling carried over (both formats)
47
+
48
+ Fill colour, stroke colour, stroke width, dashed/dotted strokes, rounded corners, arrowhead types, opacity, background colour, multi-line text, font family, font size, and text alignment.
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install excalidraw-convert
56
+ ```
57
+
58
+ For SVG image support in PowerPoint output (optional):
59
+ ```bash
60
+ pip install "excalidraw-convert[svg]"
61
+ ```
62
+
63
+ To install from source:
64
+ ```bash
65
+ pip install -e ".[dev]"
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Usage
71
+
72
+ ### Command line
73
+
74
+ ```bash
75
+ # Convert to PowerPoint
76
+ excalidraw-convert convert "https://excalidraw.com/#json=<id>,<key>" -o output.pptx
77
+
78
+ # Convert to Lucid Package Format
79
+ excalidraw-convert convert-lucid "https://excalidraw.com/#json=<id>,<key>" -o output.lucid
80
+
81
+ # From a direct JSON / .excalidraw file URL
82
+ excalidraw-convert convert "https://raw.githubusercontent.com/you/repo/main/diagram.excalidraw" -o output.pptx
83
+
84
+ # Append slides to an existing presentation
85
+ excalidraw-convert convert "https://..." -o existing.pptx --embed
86
+
87
+ # Inspect a scene before converting
88
+ excalidraw-convert inspect "https://..."
89
+ ```
90
+
91
+ Also callable as a module:
92
+ ```bash
93
+ python -m excalidraw_convert convert "https://..." -o output.pptx
94
+ python -m excalidraw_convert convert-lucid "https://..." -o output.lucid
95
+ ```
96
+
97
+ ### Python API
98
+
99
+ ```python
100
+ from excalidraw_convert import fetch_scene, convert, convert_to_lucid
101
+
102
+ scene = fetch_scene("https://excalidraw.com/#json=<id>,<key>")
103
+
104
+ # PowerPoint
105
+ convert(scene, "output.pptx")
106
+
107
+ # Lucid Package Format
108
+ convert_to_lucid(scene, "output.lucid")
109
+
110
+ # Append to an existing PowerPoint file
111
+ convert(scene, "existing.pptx", embed_path="existing.pptx")
112
+
113
+ # Custom slide dimensions (inches, new files only)
114
+ convert(scene, "output.pptx", slide_width=10, slide_height=7.5)
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Supported URL formats
120
+
121
+ | Format | Example |
122
+ |---|---|
123
+ | Excalidraw share link | `https://excalidraw.com/#json=abc123,base64key` |
124
+ | Raw `.excalidraw` file URL | `https://raw.githubusercontent.com/.../diagram.excalidraw` |
125
+ | Any URL returning JSON | `https://your-server.com/scene.json` |
126
+
127
+ > **Note on Excalidraw share links:** the scene is end-to-end encrypted in the browser. This tool replicates the AES-GCM decryption that the Excalidraw web app performs client-side. If decryption fails for a link, try exporting the scene as a `.excalidraw` file and hosting it somewhere accessible.
128
+
129
+ ---
130
+
131
+ ## Limitations / known gaps
132
+
133
+ ### Both formats
134
+ - **Roughness / hand-drawn style**: Excalidraw's sketchy aesthetic is not reproduced — shapes are rendered as clean geometric shapes.
135
+ - **Hachure fill**: approximated as a solid fill.
136
+ - **Curved / elbow arrows**: rendered as straight connectors or straight segments.
137
+ - **`freedraw` pressures**: variable stroke-width pressure data is ignored; a fixed width is used.
138
+ - **Grouped elements**: group membership is preserved in rendering order but not as native groups.
139
+ - **Embeddable / iframe elements**: skipped with a warning.
140
+ - **Fonts**: Excalidraw's *Virgil* hand-drawn font is mapped to *Caveat* (install it for best results; falls back to the system default).
141
+
142
+ ### PowerPoint only
143
+ - `freedraw` elements are rendered as freeform polygon paths (not true brush strokes).
144
+
145
+ ### Lucid only
146
+ - `freedraw` elements are approximated as `flexiblePolygon` shapes with up to 100 sampled vertices.
147
+ - Arrow connection points snap to the nearest edge of the target shape.
148
+
149
+ ---
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ pip install -e ".[dev]"
155
+ pytest tests/ -v
156
+ ```
@@ -0,0 +1,11 @@
1
+ """
2
+ excalidraw-convert: Convert Excalidraw diagrams to editable PowerPoint vector objects
3
+ or Lucid Package Format (.lucid) files.
4
+ """
5
+
6
+ from .converter import convert
7
+ from .fetcher import fetch_scene
8
+ from .lucid_converter import convert_to_lucid
9
+
10
+ __all__ = ["convert", "fetch_scene", "convert_to_lucid"]
11
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,186 @@
1
+ """
2
+ CLI interface for excalidraw-convert.
3
+
4
+ Usage examples:
5
+ # Convert to PowerPoint
6
+ excalidraw-convert convert "https://excalidraw.com/#json=abc,key" -o output.pptx
7
+
8
+ # Append slides to an existing .pptx
9
+ excalidraw-convert convert "https://..." -o existing.pptx --embed
10
+
11
+ # Convert to Lucid Package Format
12
+ excalidraw-convert convert-lucid "https://excalidraw.com/#json=abc,key" -o output.lucid
13
+
14
+ # Also available as: python -m excalidraw_convert convert "..."
15
+ """
16
+
17
+ import sys
18
+
19
+ import click
20
+
21
+ from .fetcher import fetch_scene
22
+ from .converter import convert
23
+ from .lucid_converter import convert_to_lucid
24
+
25
+
26
+ @click.group()
27
+ @click.version_option(package_name="excalidraw-convert")
28
+ def main():
29
+ """Convert Excalidraw diagrams to PowerPoint or Lucid Package Format."""
30
+
31
+
32
+ @main.command()
33
+ @click.argument("url")
34
+ @click.option(
35
+ "-o",
36
+ "--output",
37
+ default="output.pptx",
38
+ show_default=True,
39
+ help="Path of the output .pptx file.",
40
+ )
41
+ @click.option(
42
+ "--embed",
43
+ is_flag=True,
44
+ default=False,
45
+ help=(
46
+ "Append the converted slides to an existing file instead of creating "
47
+ "a new one. Requires --output to point at an existing .pptx."
48
+ ),
49
+ )
50
+ @click.option(
51
+ "--slide-width",
52
+ default=13.33,
53
+ show_default=True,
54
+ type=float,
55
+ help="Slide width in inches (new presentations only).",
56
+ )
57
+ @click.option(
58
+ "--slide-height",
59
+ default=7.5,
60
+ show_default=True,
61
+ type=float,
62
+ help="Slide height in inches (new presentations only).",
63
+ )
64
+ def convert_cmd(url: str, output: str, embed: bool, slide_width: float, slide_height: float):
65
+ """
66
+ Convert an Excalidraw diagram at URL to a PowerPoint file.
67
+
68
+ URL can be:
69
+
70
+ \b
71
+ • An Excalidraw share link: https://excalidraw.com/#json=<id>,<key>
72
+ • A direct JSON/file URL: https://example.com/diagram.excalidraw
73
+ """
74
+ click.echo(f"Fetching scene from: {url}")
75
+ try:
76
+ scene = fetch_scene(url)
77
+ except Exception as exc:
78
+ click.echo(f"Error fetching scene: {exc}", err=True)
79
+ sys.exit(1)
80
+
81
+ n_elements = len([e for e in scene.get("elements", []) if not e.get("isDeleted")])
82
+ n_frames = len([e for e in scene.get("elements", []) if e["type"] == "frame" and not e.get("isDeleted")])
83
+ click.echo(
84
+ f"Scene loaded: {n_elements} elements, "
85
+ f"{n_frames} frame(s) -> {max(n_frames, 1)} slide(s)"
86
+ )
87
+
88
+ embed_path = output if embed else None
89
+
90
+ try:
91
+ convert(
92
+ scene,
93
+ output_path=output,
94
+ embed_path=embed_path,
95
+ slide_width=slide_width,
96
+ slide_height=slide_height,
97
+ )
98
+ except Exception as exc:
99
+ click.echo(f"Error converting scene: {exc}", err=True)
100
+ sys.exit(1)
101
+
102
+ click.echo(f"Saved: {output}")
103
+
104
+
105
+ @main.command()
106
+ @click.argument("url")
107
+ def inspect(url: str):
108
+ """Print a summary of the Excalidraw scene without converting."""
109
+ click.echo(f"Fetching: {url}")
110
+ try:
111
+ scene = fetch_scene(url)
112
+ except Exception as exc:
113
+ click.echo(f"Error: {exc}", err=True)
114
+ sys.exit(1)
115
+
116
+ elements = [e for e in scene.get("elements", []) if not e.get("isDeleted")]
117
+
118
+ from collections import Counter
119
+ counts = Counter(e["type"] for e in elements)
120
+
121
+ click.echo(f"\nElement counts:")
122
+ for etype, count in sorted(counts.items()):
123
+ click.echo(f" {etype:20s} {count}")
124
+
125
+ frames = [e for e in elements if e["type"] == "frame"]
126
+ if frames:
127
+ click.echo(f"\nFrames ({len(frames)}):")
128
+ for f in frames:
129
+ click.echo(
130
+ f" [{f.get('name', '(unnamed)')}] "
131
+ f"{f['width']:.0f}×{f['height']:.0f} px "
132
+ f"@ ({f['x']:.0f}, {f['y']:.0f})"
133
+ )
134
+ else:
135
+ click.echo("\nNo frames found — whole canvas will become one slide.")
136
+
137
+ files = scene.get("files", {})
138
+ if files:
139
+ click.echo(f"\nEmbedded files: {len(files)}")
140
+
141
+
142
+ @main.command("convert-lucid")
143
+ @click.argument("url")
144
+ @click.option(
145
+ "-o",
146
+ "--output",
147
+ default="output.lucid",
148
+ show_default=True,
149
+ help="Path of the output .lucid file.",
150
+ )
151
+ def convert_lucid_cmd(url: str, output: str):
152
+ """
153
+ Convert an Excalidraw diagram at URL to a Lucid Package Format (.lucid) file.
154
+
155
+ URL can be:
156
+
157
+ \b
158
+ • An Excalidraw share link: https://excalidraw.com/#json=<id>,<key>
159
+ • A direct JSON/file URL: https://example.com/diagram.excalidraw
160
+ """
161
+ click.echo(f"Fetching scene from: {url}")
162
+ try:
163
+ scene = fetch_scene(url)
164
+ except Exception as exc:
165
+ click.echo(f"Error fetching scene: {exc}", err=True)
166
+ import sys
167
+ sys.exit(1)
168
+
169
+ n_elements = len([e for e in scene.get("elements", []) if not e.get("isDeleted")])
170
+ n_frames = len([
171
+ e for e in scene.get("elements", [])
172
+ if e["type"] == "frame" and not e.get("isDeleted")
173
+ ])
174
+ click.echo(
175
+ f"Scene loaded: {n_elements} elements, "
176
+ f"{n_frames} frame(s) → {max(n_frames, 1)} page(s)"
177
+ )
178
+
179
+ try:
180
+ convert_to_lucid(scene, output_path=output)
181
+ except Exception as exc:
182
+ click.echo(f"Error converting scene: {exc}", err=True)
183
+ import sys
184
+ sys.exit(1)
185
+
186
+ click.echo(f"Saved: {output}")