Skip to content

Commit 4028e98

Browse files
committed
pm_slide_explort: option to export a functioning HTML page instead of PNG previews
Uses the HTML previews and glues many of them together to a single document, peppered with a bit of JavaScript for slide navigation. The resulting document still needs resources from the original webserver, so it's not fully stand-alone! It's rather meant as a backup for when Slidemeister acts up (which is exactly what it did the night before Deadline 2024, hence this option ...)
1 parent 9805ce8 commit 4028e98

File tree

1 file changed

+168
-21
lines changed

1 file changed

+168
-21
lines changed

src/pm-export-tools/pm_slide_export.py

Lines changed: 168 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def canonicalize(x):
2424
[default: read from stdin]""")
2525
parser.add_argument("-o", "--outdir", metavar="DIR",
2626
help="output directory [default: 'slides' subdirectory of the script's directory]")
27+
parser.add_argument("-H", "--html", action='store_true',
28+
help="export HTML preview instead of PNG")
2729
parser.add_argument("-c", "--clean", action='store_true',
2830
help="delete output directory before downloading (DANGEROUS!)")
2931
parser.add_argument("-n", "--dry-run", action='store_true',
@@ -64,11 +66,13 @@ def canonicalize(x):
6466

6567
# our super-simplistic, very special-cased parser
6668
html = html.split("<tbody", 1)[-1]
69+
docs = {}
6770
for attrs, tr in re.findall(r'<tr([^>]*)>(.*?)</tr>', html, flags=re.I+re.S):
6871
row = [re.sub(r'<[^>]+>', '', td).strip()
6972
for attrs, td
7073
in re.findall(r'<td([^>]*)>(.*?)</td>', tr, flags=re.I+re.S)]
71-
url = re.search(r'href="(https?://.*?\.(png|jpe?g))"', tr, flags=re.I)
74+
re_ext = "html?" if args.html else "png|jpe?g"
75+
url = re.search(r'href="(https?://.*?\.(' + re_ext + '))"', tr, flags=re.I)
7276
if url: url = url.group(1)
7377
if not(url) or (len(row) < 5):
7478
print(f"WARNING: invalid slide {row[:5]}", file=sys.stderr)
@@ -86,32 +90,175 @@ def canonicalize(x):
8690
+ "_" + pg_slide.group(2)
8791
else:
8892
digits_at_end = re.search(r'(\d+)$', name)
89-
if ("coming" in stype) and (("coming" in name) or ("now" in name)):
93+
if (("coming" in stype) or ("now" in stype)) and (("coming" in name) or ("now" in name)):
9094
name = "00_" + name
91-
if digits_at_end and ("competition" in stype):
95+
if digits_at_end and (("competition" in stype) or ("compo_entry" in stype)):
9296
name = digits_at_end.group(1).rjust(2, '0')
9397
if ("end" in name) and ("end" in stype):
9498
name = "99_end"
9599

96-
# download
97-
outdir = os.path.join(basedir, folder)
98-
if not(os.path.isdir(outdir)) and not(args.dry_run):
100+
# download PNG
101+
if not args.html:
102+
outdir = os.path.join(basedir, folder)
103+
if not(os.path.isdir(outdir)) and not(args.dry_run):
104+
try:
105+
os.makedirs(outdir)
106+
except EnvironmentError as e:
107+
print(f"ERROR: can't create directory '{outdir}':", e, file=sys.stderr)
108+
continue
109+
outfile = os.path.join(outdir, name + ext)
110+
if args.verbose:
111+
print(url)
112+
if args.dry_run:
113+
print("=>", outfile)
114+
else:
115+
print("=>", outfile, end=' ')
116+
sys.stdout.flush()
117+
try:
118+
with urllib.request.urlopen(url, context=ssl_ctx) as f_in, open(outfile, 'wb') as f_out:
119+
f_out.write(f_in.read())
120+
print("[OK]")
121+
except EnvironmentError as e:
122+
print(e)
123+
124+
# handle HTML
125+
if args.html:
126+
if not folder in docs:
127+
docs[folder] = {}
128+
if args.dry_run:
129+
print(f"{folder}/{name} <= {url}")
130+
else:
131+
print(f"<= {folder}/{name}", end=' ')
132+
try:
133+
with urllib.request.urlopen(url, context=ssl_ctx) as f_in:
134+
docs[folder][name] = f_in.read().decode('utf-8', 'replace')
135+
print("[OK]")
136+
except EnvironmentError as e:
137+
print(e)
138+
139+
# create HTML output
140+
if args.html and not(args.dry_run):
141+
if not os.path.isdir(basedir):
99142
try:
100-
os.makedirs(outdir)
143+
os.makedirs(basedir)
101144
except EnvironmentError as e:
102-
print(f"ERROR: can't create directory '{outdir}':", e, file=sys.stderr)
103-
continue
104-
outfile = os.path.join(outdir, name + ext)
105-
if args.verbose:
106-
print(url)
107-
if args.dry_run:
108-
print("=>", outfile)
109-
else:
145+
print(f"ERROR: can't create directory '{basedir}':", e, file=sys.stderr)
146+
sys.exit(1)
147+
148+
domain = url[:10] + url[10:].split('/', 1)[0] + '/'
149+
150+
for compo, cdata in docs.items():
151+
outfile = os.path.join(basedir, compo + ".html")
110152
print("=>", outfile, end=' ')
111-
sys.stdout.flush()
112-
try:
113-
with urllib.request.urlopen(url, context=ssl_ctx) as f_in, open(outfile, 'wb') as f_out:
114-
f_out.write(f_in.read())
153+
154+
doc = list(cdata.values())[0]
155+
header, doc = doc.split('<div id="slidemeister">', 1)
156+
footer = '<script>' + doc.split('<script>', 1)[-1]
157+
158+
header = header.replace('href="/', 'href="' + domain)
159+
160+
doc = header.replace('</style>', """
161+
.exported_off { display:none !important; }
162+
</style>""")
163+
164+
if os.path.exists(os.path.join(basedir, "patch.js")):
165+
doc += """
166+
<canvas id="glcanvas" width="100vw" height="100vh" tabindex="1"></canvas>
167+
<script type="text/javascript" src="patch.js" async></script>
168+
<script>
169+
function showError(errId, errMsg)
170+
{
171+
console.log("Cables error", errId, ":", errMsg);
172+
}
173+
document.addEventListener("CABLES.jsLoaded", function (event)
174+
{
175+
CABLES.patch = new CABLES.Patch({
176+
patch: CABLES.exportedPatch,
177+
"prefixAssetPath": "",
178+
"assetPath": "assets/",
179+
"jsPath": "",
180+
"glCanvasId": "glcanvas",
181+
"glCanvasResizeToWindow": true,
182+
"onError": showError,
183+
"canvas": {"alpha":true, "premultipliedAlpha":true } // make canvas transparent
184+
});
185+
});
186+
</script>
187+
"""
188+
189+
div_attrs = 'class="exported_slide" id="slidemeister"'
190+
for name, subdoc in sorted(cdata.items()):
191+
subdoc = subdoc.split('<div id="slidemeister">', 1)[-1]
192+
if name.isdigit():
193+
stype = "compo"
194+
else:
195+
stype = ''.join(c for c in name if c.isalpha())
196+
div_attrs += f' data-slide-type="{stype}"'
197+
doc += f"<div {div_attrs}>" + subdoc.split('<script>', 1)[0]
198+
div_attrs = 'class="exported_slide exported_off"'
199+
200+
doc += footer.replace('</script>', """
201+
var transitionFrom = null;
202+
var transitionTo = null;
203+
const transitionDuration = 1.0; // seconds
204+
const opacityStep = 2.0 / (60 * transitionDuration);
205+
var fadeOpacity = null;
206+
207+
function fadeInHandler() {
208+
fadeOpacity += opacityStep;
209+
if (fadeOpacity >= 1.0) { fadeOpacity = 1.0; }
210+
transitionTo.style.opacity = fadeOpacity;
211+
if (fadeOpacity >= 1.0) {
212+
console.log("transition finished");
213+
} else {
214+
window.requestAnimationFrame(fadeInHandler);
215+
}
216+
}
217+
218+
function fadeOutHandler() {
219+
fadeOpacity -= opacityStep;
220+
if (fadeOpacity <= 0.0) { fadeOpacity = 0.0; }
221+
transitionFrom.style.opacity = fadeOpacity;
222+
if (fadeOpacity <= 0.0) {
223+
transitionFrom.classList.add("exported_off");
224+
transitionFrom.id = "noid";
225+
transitionTo.style.opacity = 0.0;
226+
transitionTo.id = "slidemeister";
227+
transitionTo.classList.remove("exported_off");
228+
CABLES.patch.setVariable("SLIDETYPE", transitionTo.dataset.slideType);
229+
window.requestAnimationFrame(fadeInHandler);
230+
} else {
231+
window.requestAnimationFrame(fadeOutHandler);
232+
}
233+
}
234+
235+
function startTransition(from, to) {
236+
transitionFrom = from;
237+
transitionTo = to;
238+
if (!from || !to) {
239+
console.log("no transition possible");
240+
return;
241+
}
242+
transitionFrom.style.opacity = fadeOpacity = 1.0;
243+
window.requestAnimationFrame(fadeOutHandler);
244+
}
245+
246+
window.addEventListener('keydown', (event) => {
247+
var prevSlide = null;
248+
var currentSlide = null;
249+
var nextSlide = null;
250+
const slides = document.querySelectorAll(".exported_slide");
251+
for (var i = 0; i < slides.length; ++i) {
252+
const slide = slides[i];
253+
if (currentSlide && !nextSlide) { nextSlide = slide; }
254+
if (slide.id == "slidemeister") { currentSlide = slide; }
255+
if (!currentSlide) { prevSlide = slide; }
256+
}
257+
if (event.code == "ArrowLeft") startTransition(currentSlide, prevSlide);
258+
if (event.code == "ArrowRight") startTransition(currentSlide, nextSlide);
259+
});
260+
</script>""")
261+
262+
with open(outfile, 'w', encoding='utf-8') as f:
263+
f.write(doc)
115264
print("[OK]")
116-
except EnvironmentError as e:
117-
print(e)

0 commit comments

Comments
 (0)