-
-
Save dreness/b5ebd1ebff092b124d343164c6217a98 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
# Instructions: | |
# 1) install svgpathtools with the command: | |
# pip install svgpathtools | |
# 2) Run this script with no arguments: | |
# python keynote_shape_to_svg.py | |
from svgpathtools import parse_path, svg2paths, wsvg | |
import sqlite3 | |
import hashlib | |
import json | |
import sys | |
import os | |
KeynoteShapesPath = "/Applications/Keynote.app/Contents/Resources/shape_library.json" | |
TSKPATH = "".join( | |
[ | |
os.environ.get("HOME"), | |
"/Library/Containers/com.apple.iWork.Keynote/", | |
"Data/Library/Application Support/", | |
"com.apple.iWork.CloudKitStorage/", | |
"com.apple.iWork.TSKCloudKitPrivateZone.db", | |
] | |
) | |
OUTDIR = f"{os.environ.get('HOME')}/Pictures/keynote_shape_extracts/" | |
def writeSVG(shape, shapeName, OUTDIR): | |
if not os.path.isdir(OUTDIR): | |
try: | |
os.makedirs(OUTDIR, exist_ok=True) | |
print(f"Created {OUTDIR}") | |
except OSError as e: | |
print(f"Couldn't create output directory at {OUTDIR}!\n{e}") | |
sys.exit(1) | |
if shapeName is None or shapeName == "": | |
shapeName = hashlib.sha1(shape.encode("utf-8")).hexdigest() | |
wsvg( | |
parse_path(shape), | |
["black"], | |
attributes=[{"fill": "black"}], | |
filename=f"{OUTDIR}/{shapeName}.svg", | |
) | |
def writeShapes(shapeData, OUTDIR): | |
# shapeData is a list of (shape, shapeName) tuples | |
wrote = 0 | |
for row in shapeData: | |
shape = row[0] | |
shapeName = row[1] | |
writeSVG(shape, shapeName, OUTDIR) | |
wrote += 1 | |
print(f"Exported {wrote} keynote shapes as SVGs to {OUTDIR}") | |
def getUserShapes(TSKPATH=TSKPATH): | |
con = sqlite3.connect(TSKPATH) | |
cur = con.cursor() | |
if cur is None: | |
print(f"Couldn't access sqlite DB at {TSKPATH}") | |
sys.exit(1) | |
else: | |
print(f"Opened Keynote user library:\n{TSKPATH}") | |
# The table that holds the user shapes has this schema: | |
# CREATE TABLE tsduserdefinedshapelibraryshape | |
# ( | |
# identifier TEXT PRIMARY KEY, | |
# cloudkitmetadata TEXT, | |
# needs_first_fetch INTEGER DEFAULT 0, | |
# tsduserdefinedshapelibrarybezierpathstringkey TEXT, | |
# tsduserdefinedshapelibrarynamekey TEXT, | |
# position REAL | |
# ) | |
Q = """ | |
SELECT | |
tsduserdefinedshapelibrarybezierpathstringkey, | |
tsduserdefinedshapelibrarynamekey | |
FROM | |
tsduserdefinedshapelibraryshape; | |
""" | |
l = list(cur.execute(Q)) | |
cur.close() | |
return l | |
def getKeynoteShapes(OUTDIR=OUTDIR, KeynoteShapesPath=KeynoteShapesPath): | |
d = dict(json.loads(open(KeynoteShapesPath, "r").read())) | |
outList = [] | |
l = d.get("shapesByID") | |
for k, v in l.items(): | |
shapeName = v.get("localizationKey") | |
shape = v.get("shapePath") | |
# print(shapeName, shape) | |
outList.append((shape, shapeName)) | |
return outList | |
writeShapes(outList, OUTDIR) | |
def main(): | |
print("Looking for built-in shapes...") | |
writeShapes(getKeynoteShapes(), OUTDIR) | |
print("Looking for user shapes...") | |
writeShapes(getUserShapes(), OUTDIR) | |
if __name__ == "__main__": | |
main() |
Hi, sorry for the slow reply, @MikeiLL. I’ve never tried manually editing the user shapes library (outside of Keynote, I mean), but I’m pretty sure it is designed to support “external” updates, as the data is synced with icloud - this is how shapes you add on a Mac are visible in iOS, for example.
Perhaps you noticed the scare-quotes above. I could imagine that Apple might take steps to prevent manual edits to this data from being recognized as valid to Keynote and / or iCloud (eg a signed checksum wrapped in a public key trusted by keynote / iCloud), but there’s only one way to find out :)
(a few minutes later)
(a few minutes later)
... actually SVG2Keynote-gui is busted on Apple Silicon. There are several problems to work through, but according to this comment, it seems possible.
Nice gist, @dreness. I'm actually trying to go the other way. Do you happen to know if Keynote will honor externally made updates made to
TSKPATH
?