Spaces:
Runtime error
Runtime error
File size: 7,035 Bytes
fa8df4c 4a3d0a5 fa8df4c 4a3d0a5 fa8df4c 4a3d0a5 fa8df4c 4a3d0a5 fa8df4c 4a3d0a5 fa8df4c 4a3d0a5 fa8df4c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | """From a written deck to what is stored and shown. `finish` picks each slide's variant, fits its text, and checks
the result in Chrome when one is installed. `restructure` takes a deck back from the editor and fits it again.
`view` reads a stored deck back (clamped again, so older or edited decks still draw) and gives its pages; one slide
that cannot be drawn becomes a plain one instead of breaking the deck."""
import copy
import logging
import random
from collections import Counter
from app.infographics.image import ImageUnavailable
from app.slides import export
from app.slides.content import clamp_deck, clamp_slide, empty_slide
from app.slides.layout import GEOMETRY, SCALES, VARIANTS, assign_variants, compose, fits_variant, seed_of
from app.slides.render import slide_html
from app.slides.themes import theme_id
log = logging.getLogger(__name__)
def fit(deck: dict, theme: str, fmt: str, seed: int) -> dict:
"""Every slide as it will be drawn: text shortened where it had to be, so what is stored is what is shown."""
out = copy.deepcopy(deck)
out["slides"] = [compose(out, i, theme, fmt, seed).slide for i in range(len(out["slides"]))]
return out
def finish(deck: dict, theme: str, fmt: str, seed: int, previous: list[tuple[str, str]] | None = None) -> dict:
"""Variants, then refit. Raises ValueError when a slide cannot be drawn at all."""
theme = theme_id(theme)
deck = copy.deepcopy(deck)
assign_variants(deck["slides"], theme, seed, previous)
return refit(deck, theme, fmt, seed)
def refit(deck: dict, theme: str, fmt: str, seed: int) -> dict:
"""Fitted text and a Chrome check that no text box overflows (slides that do are drawn a step smaller and fitted
again), keeping each slide's variant. Raises ValueError when a slide cannot be drawn at all."""
theme = theme_id(theme)
deck = fit(deck, theme, fmt, seed)
for _ in range(3):
try:
bad = export.overflowing(deck, theme, fmt, seed)
except ImageUnavailable:
break # no Chrome here: the measured fit alone keeps text inside its boxes
if not bad:
break
log.info("slides %s overflow in Chrome, drawing them smaller", sorted(bad))
for i in bad:
deck["slides"][i]["shrink"] = min(int(deck["slides"][i].get("shrink") or 0) + 1, len(SCALES) - 1)
deck = fit(deck, theme, fmt, seed)
for i in range(len(deck["slides"])):
slide_html(deck, i, theme, fmt, seed) # raises if a slide cannot be drawn: better now than when shown
return deck
class BadEdit(ValueError):
"""A deck sent back from the editor that cannot be kept (no slides, or too many)."""
MAX_SLIDES = 30
def fill_variants(slides: list[dict], theme: str, seed: int) -> None:
"""Give a variant, in place, to each slide that has none or whose variant no longer suits it (a new slide, a changed
layout, a card row now with five points). Every other slide keeps its own, so editing one slide never redraws the
rest. The choice follows assign_variants: suited to the content, not the same as a neighbour, unused ones first."""
theme = theme_id(theme)
def ok(i: int) -> bool:
s = slides[i]
return s["variant"] in VARIANTS[s["layout"]] and fits_variant(s, theme, s["variant"])
keep = [ok(i) for i in range(len(slides))]
for i, s in enumerate(slides):
before = (slides[i - 1]["layout"], slides[i - 1]["variant"]) if i else None
if keep[i] and (s["layout"], s["variant"]) != before:
continue
after = (slides[i + 1]["layout"], slides[i + 1]["variant"]) if i + 1 < len(slides) and keep[i + 1] else None
options = [v for v in VARIANTS[s["layout"]] if fits_variant(s, theme, v)] or list(VARIANTS[s["layout"]])
fresh = [v for v in options if (s["layout"], v) not in (before, after)] or options
used = Counter((x["layout"], x["variant"]) for j, x in enumerate(slides) if j != i)
rnd = random.Random(seed + i)
s["variant"] = min(fresh, key=lambda v: (used[(s["layout"], v)], rnd.random()))
keep[i] = True
def restructure(content: dict, slides: object, final: bool) -> dict:
"""The stored deck with the slides the editor sent (text edited, slides added, removed, moved, or changed to another
layout). They are coerced like model output (clamp_deck: plain text, the format's limits, valid layouts), slides
that need one get a variant, and the deck is fitted: quickly for a preview, with the Chrome check when `final`."""
got = stored(content)
if not got:
raise BadEdit("This deck has no slides to edit.")
deck, theme, fmt, seed = got
if not isinstance(slides, list) or not slides:
raise BadEdit("A deck needs at least one slide.")
if len(slides) > MAX_SLIDES:
raise BadEdit(f"A deck can have at most {MAX_SLIDES} slides.")
new = clamp_deck({"title": deck["title"], "subtitle": deck["subtitle"], "slides": slides}, fmt)
if not new:
raise BadEdit("A deck needs at least one slide with a heading.")
for s in new["slides"]:
s["shrink"] = 0 # text changed: fitted afresh, so shorter text can be drawn larger again
fill_variants(new["slides"], theme, seed)
return refit(new, theme, fmt, seed) if final else fit(new, theme, fmt, seed)
def slots(content: dict) -> list[list[str]]:
"""For each slide, the parts its variant has a place for (kicker, lead, body, by, closing...): what the editor
can offer to add."""
got = stored(content)
if not got:
return []
deck, theme, _fmt, _seed = got
out = []
for s in deck["slides"]:
variants = GEOMETRY[theme][s["layout"]]
out.append(sorted(k for k, v in (variants.get(s["variant"]) or next(iter(variants.values()))).items() if v))
return out
def stored(content: dict) -> tuple[dict, str, str, int] | None:
"""A stored deck, clamped, with its theme, format and seed (None when there is none)."""
fmt = content.get("format") if content.get("format") in ("detailed", "presenter") else "detailed"
deck = clamp_deck(content.get("deck") or {}, fmt)
if not deck:
return None
return deck, theme_id(content.get("theme")), fmt, seed_of(content.get("seed"))
def _placeholder(deck: dict, index: int) -> dict:
s = empty_slide("section")
s["heading"] = deck.get("title") or "Slide"
return clamp_slide(s, "detailed")
def view(content: dict) -> list[str]:
"""Each slide of a stored deck as a standalone page."""
got = stored(content)
if not got:
return []
deck, theme, fmt, seed = got
pages = []
for i in range(len(deck["slides"])):
try:
pages.append(slide_html(deck, i, theme, fmt, seed))
except Exception:
log.exception("slide %s could not be drawn", i)
safe = copy.deepcopy(deck)
safe["slides"][i] = _placeholder(deck, i)
pages.append(slide_html(safe, i, theme, fmt, seed))
return pages
|