md2star.pptx_fit module

Does this content actually fit this layout? — arithmetic, not taste.

The layout selector in md2star.pptx_layout asks an LLM to match a slide to one of the designer’s layouts, and a VLM to confirm the pick visually. Both stages were given only the slide’s title and a list of categorical cues (“has a table”, “has bullets”). Neither was told how much content there is, and neither was told how large the chosen layout’s placeholders are — even though the catalog already extracts every placeholder’s box in EMU.

So the system could cheerfully put nine long bullets into a layout the designer drew for three short ones. Nothing failed; PowerPoint just shrank the text, or let it spill past the placeholder. The deck came out looking unlike the template it was supposedly built from.

This module closes that gap by making the question measurable.

The missing constant: a minimum font size

“Does this text fit?” has no answer until you fix how small the text is allowed to get — shrink far enough and anything fits, which is exactly the failure above.

We take the floor from Guy Kawasaki’s 10/20/30 rule: ten slides, twenty minutes, thirty point type. The thirty is the operative part here. Once a font floor exists, a placeholder of known EMU dimensions has a computable capacity, and “will this fit?” becomes arithmetic rather than judgement.

The thirty is a target, not a gate. Plenty of legitimate decks cannot hold to it — a technical appendix, a training deck, a slide that genuinely carries a dense table. So overflow past the floor is scored as a graded penalty and reported in words, never used to disqualify a layout outright. The only hard refusals are structural and have nothing to do with type size: an image with nowhere to put it, or body text with no body placeholder at all. Those are not “too much content”, they are “the wrong shape of slide”.

A caller who does want the rule enforced passes strict=True and gets disqualification instead of a penalty.

How capacity is estimated

From the placeholder box, the font floor, and two typographic constants:

  • AVG_CHAR_WIDTH_EM = 0.5 — mean advance width of a character in a sans-serif face, as a fraction of the em. The widely used figure for Arial/Helvetica/Inter-class faces; CSS’s ch unit (the width of “0”) runs wider, which is why it is not used here.

  • LINE_SPACING = 1.2 — single line spacing as OOXML renders it.

Both are approximations, deliberately. The alternative is real font metrics, which means shipping or locating the template’s actual font files; python-pptx’s own fit_text takes that route and requires the caller to hand it a font path. For choosing between layouts a consistent estimate is worth more than an exact one: every candidate is measured the same way, so the ranking holds even where the absolute numbers drift. FIT_SLACK keeps the decision off the knife edge.

How candidates are scored

Two ideas from the literature, plus one from the brief:

  • Occupancy matching. SlideGen (arXiv 2512.04529) scores layout aesthetics with a geometry-aware density term 1 - |ρ - τ|, where ρ is the fraction of available space the content occupies and τ ≈ 0.55 the target — calibrated against 40 human raters (ρ = 0.82 correlation with their preferences). A slide at 15 % occupancy looks abandoned; at 95 % it looks crammed. We reuse the term directly.

  • Element-count and aspect matching. SlideGen’s Arranger picks “based on the number and types of elements, and on the size and aspect ratio of the visuals”. We match image count against pic placeholder count, and a slide’s image against whether the layout has anywhere to put one.

  • Image-forward, text-sparse. A slide that is one image and few words is the target, not a fallback. Sparse text on an image-bearing layout is rewarded; an image with nowhere to go is penalised hard.

Nothing here applies styling. The design stays the designer’s; this only decides which of their slides a given piece of content belongs on, and says so with numbers that can be argued with.

Author

Warith HARCHAOUI, https://linkedin.com/in/warith-harchaoui

md2star.pptx_fit.ALIGNMENT_TOLERANCE = 0.01

How close two edges must be to read as deliberately aligned, as a fraction of the canvas.

md2star.pptx_fit.ASPECT_OCTAVE_TOLERANCE = 1.0

Beyond this many octaves of aspect difference, the image is being seriously letterboxed or cropped. One octave = the box is twice as wide, relatively, as the image (or vice versa).

md2star.pptx_fit.AVG_CHAR_WIDTH_EM = 0.5

Mean character advance as a fraction of the em, for a sans-serif face.

md2star.pptx_fit.BALANCE_TOLERANCE = 0.45

How far the centre of mass may drift before the slide reads as lopsided, as a fraction of the half-diagonal. Set above a rule-of-thirds placement (~0.33) on purpose: see balance() for why that matters.

md2star.pptx_fit.CAPTION_CHARS = 140

How much text a title-only layout can absorb as a caption before it stops being a plausible host. Roughly one spoken line: the “few words” half of “one image and few words”.

class md2star.pptx_fit.Capacity(lines=0, chars_per_line=0)[source]

Bases: object

What one placeholder box can hold at a given font floor.

Parameters:
  • lines (int)

  • chars_per_line (int)

property chars: int

Total characters the box holds at the floor font size.

chars_per_line: int = 0
lines: int = 0
class md2star.pptx_fit.ContentMetrics(title_chars=0, body_chars=0, bullet_count=0, longest_bullet_chars=0, image_count=0, table_rows=0, table_cols=0, code_lines=0, image_paths=<factory>, big_statement=False, columns=0, bullet_lengths=<factory>)[source]

Bases: object

How much of what a slide actually holds.

Counts visible text, not markup: the question is what a reader sees, so fence bodies, image syntax and bullet markers do not inflate the totals.

Parameters:
  • title_chars (int)

  • body_chars (int)

  • bullet_count (int)

  • longest_bullet_chars (int)

  • image_count (int)

  • table_rows (int)

  • table_cols (int)

  • code_lines (int)

  • image_paths (list[str])

  • big_statement (bool)

  • columns (int)

  • bullet_lengths (list[int])

big_statement: bool = False

The author marked this slide as a single large statement ({.big}). Structural, not stylistic: it wants a title-only layout, with no body box sitting empty underneath it.

body_chars: int = 0
bullet_count: int = 0
bullet_lengths: list[int]

Visible length of each list item, in order. A bullet is set on lines of its own, so its last line is charged whole: ten 75-character bullets in a 60-character line need twenty lines, not thirteen.

code_lines: int = 0
columns: int = 0

text in columns is spread over the layout’s body boxes, one flow of text goes into a single one.

Type:

Number of ::: {.column} divs

image_count: int = 0
image_paths: list[str]

Paths exactly as the Markdown wrote them, in document order. Kept so aspect_match() can read each image’s real dimensions; a count cannot tell a panorama from a portrait.

property is_sparse: bool

True for the one-image-and-few-words slide this suite aims at.

A table or a code block disqualifies it however few words surround them: thirteen rows of figures is not a sparse slide, and counting only prose would have called it one.

longest_bullet_chars: int = 0
table_cols: int = 0
table_rows: int = 0
title_chars: int = 0
property word_count: int

Rough word count of the body, for the sparsity reward.

md2star.pptx_fit.EMU_PER_PT = 12700

EMU per point. OOXML’s fixed conversion (914400 EMU per inch, 72 pt/inch).

md2star.pptx_fit.FIT_SLACK = 1.15

Tolerance on the fit verdict. The capacity estimate is approximate, so a slide that overruns by a few percent is not called impossible; one that overruns by a third is.

class md2star.pptx_fit.FitReport(fits=True, overruns=False, occupancy=0.0, score=0.0, capacity_chars=0, needed_chars=0, composition=0.0, reasons=<factory>)[source]

Bases: object

Whether a slide’s content can go on a layout, and how well it sits there.

The two verdicts are deliberately separate, because they answer different questions:

fits

Can this content be placed here at all? False only for structural impossibilities — an image with no picture placeholder, body text with no body placeholder. Never merely because there is a lot of text.

overruns

Does the text exceed what the layout holds at the font target? An observation, scored as a penalty and explained in reasons. A deck that overruns everywhere is still a deck; it is just not a 30 pt one.

Parameters:
capacity_chars: int = 0
composition: float = 0.0

Contribution of the compositional principles (balance, reading order, alignment, thirds). Kept separate from score so a caller can see whether a layout won on capacity or on composition.

fits: bool = True
property implied_pt: float

The font size this content would actually need to fit here.

Capacity scales with the inverse square of the point size, so the size that would fit is the target scaled by sqrt(capacity / needed). Reported rather than applied: it tells an author “this slide is a 22 pt slide” instead of silently making it one.

needed_chars: int = 0
occupancy: float = 0.0
property overflow_chars: int

Characters beyond what the layout holds at the font target.

overruns: bool = False
reasons: list[str]
score: float = 0.0
md2star.pptx_fit.KAWASAKI_MAX_SLIDES = 10

exceeded is an observation, never an error. Some decks legitimately are not pitches.

Type:

Slides per deck, same rule, same standing

md2star.pptx_fit.KAWASAKI_MIN_PT = 30.0

The font-size target, in points. Guy Kawasaki’s 10/20/30. Everything here is measured relative to this number, but it is an ideal rather than a requirement: a slide that overruns it is penalised and reported, not rejected. Raise it for a stricter deck; lower it when the content genuinely warrants more text on the slide.

md2star.pptx_fit.LINE_SPACING = 1.2

Single line spacing, as rendered.

md2star.pptx_fit.PARAGRAPH_GAP_LINES = 0.3

Vertical gap between two list items, in lines (see evaluate()).

md2star.pptx_fit.TARGET_OCCUPANCY = 0.55

Target occupancy — SlideGen’s τ. Neither abandoned nor crammed.

md2star.pptx_fit.THIRDS_TOLERANCE = 0.05

How close to a rule-of-thirds line a focal element must sit to count as placed on it, as a fraction of the canvas. A twentieth is tight enough to mean something and loose enough to tolerate a designer’s optical centring.

md2star.pptx_fit.alignment(metrics, layout, canvas)[source]

Do the filled boxes share edges, the way a designer would have set them?

Shared left edges or a shared top is what makes a slide look composed rather than assembled. This rewards a layout whose used boxes line up – which is not automatic, since a layout can be internally tidy and still put the two boxes this content needs on unrelated grids.

Parameters:
Return type:

tuple[float, str]

md2star.pptx_fit.aspect_match(metrics, layout, *, base_dir=None)[source]

Does the image’s shape match the shape of the box it would go in?

Images and picture placeholders are paired in document order, and the worst pairing decides the score – one badly letterboxed image spoils a slide regardless of how well the others sit.

Returns (0.0, "") whenever the dimensions cannot be read: a remote URL, a missing file, a path relative to a directory the caller did not supply. No signal is not the same as a bad signal, and treating it as one would penalise every deck that keeps its images somewhere this function cannot reach.

Parameters:
Return type:

tuple[float, str]

md2star.pptx_fit.balance(metrics, layout, canvas)[source]

Is the filled content piled into one corner, leaving the rest empty?

A defect detector, not a beauty contest: it returns 0 for anything reasonably placed and goes negative only when the area-weighted centre of mass is genuinely marooned. Rewarding dead-centre instead would be wrong, and provably so – it put this function in direct conflict with thirds(), which rewards the off-centre placement a designer chooses deliberately. Scoring both as preferences meant the two cancelled, and the heavier weight silently won: an image sitting exactly on a third ranked below the same image centred.

So centring earns nothing here. Only the corner case is called out, which is the one that actually looks broken.

Parameters:
Return type:

tuple[float, str]

md2star.pptx_fit.body_capacity(layout, min_pt=30.0, *, single=True)[source]

What layout’s body box holds at min_pt (its largest one, by default).

Public face of the summed capacity evaluate() charges against, so a caller looking for the template’s roomiest layout (to split a slide that fits none, see md2star.pptx_layout.paginate()) measures it the very same way the ranking does.

Parameters:
Return type:

Capacity

md2star.pptx_fit.capacity_for_prompt(layout, *, min_pt=30.0)[source]

One line of measured facts about a layout, for an LLM prompt.

Parameters:
Return type:

str

md2star.pptx_fit.composition(metrics, layout, canvas, *, base_dir=None)[source]

Combine the compositional principles into one score and its reasons.

Weights are deliberately modest relative to the fit score: composition ranks layouts that can already host the content, it does not rescue one that cannot. Returns (0.0, []) when the canvas is unknown, so a template whose size could not be read simply falls back to fit alone.

Parameters:
Return type:

tuple[float, list[str]]

md2star.pptx_fit.deck_advice(measured, *, min_pt=30.0)[source]

Kawasaki-rule observations about a whole deck.

Advice, never enforcement: a training deck or a technical appendix has every right to break these. The point is that the author chose to, rather than finding out when the type came back shrunk.

Parameters:
  • measured (list of (str, ContentMetrics)) – One (title, metrics) pair per slide, in deck order.

  • min_pt (float, default KAWASAKI_MIN_PT) – The font floor the word-count guidance is relative to.

Returns:

Human-readable notes; empty when the deck already follows the rule.

Return type:

list of str

md2star.pptx_fit.evaluate(metrics, layout, *, min_pt=30.0, strict=False, canvas=None, base_dir=None)[source]

Score how well layout can host the content described by metrics.

Parameters:
Returns:

fits is the hard verdict; score ranks the layouts that do fit. reasons explains both, in words meant for a human or a prompt.

Return type:

FitReport

Notes

fits=False is a statement about geometry, not taste: the content cannot be shown on that layout without going below the font floor. The caller is expected to exclude such layouts from the candidate set rather than rank them last.

md2star.pptx_fit.measure(title, body)[source]

Measure body (and title) into a ContentMetrics.

Parameters:
  • title (str) – The slide’s heading text, already stripped of attribute syntax.

  • body (str) – The slide’s full Markdown source, heading included.

Returns:

Counts of everything the fit computation needs.

Return type:

ContentMetrics

Examples

>>> m = measure("Results", "# Results\n\n- one\n- two\n")
>>> m.bullet_count
2
>>> m.image_count
0
md2star.pptx_fit.placeholder_capacity(emu, *, min_pt=30.0)[source]

Estimate how much text a placeholder holds at min_pt.

Parameters:
  • emu (tuple of int, or None) – (left, top, width, height) in EMU, as the catalog extracts it. None (a layout that declares a placeholder without geometry) yields an empty capacity, which the caller reads as “unknown”.

  • min_pt (float, default KAWASAKI_MIN_PT) – The font floor. Capacity scales roughly as its inverse square: half the point size holds about four times the text.

Returns:

Lines, characters per line, and their product.

Return type:

Capacity

Examples

A half-slide body box on a 16:9 deck, at 30 pt:

>>> cap = placeholder_capacity((0, 0, 4572000, 2571750))
>>> cap.lines > 0 and cap.chars_per_line > 0
True
md2star.pptx_fit.rank(metrics, layouts, *, min_pt=30.0, strict=False, canvas=None, base_dir=None)[source]

Order layouts best-first for this content.

Structurally impossible layouts sort last but are still returned with their report, so a caller can show why something was passed over instead of only what won. The list is never empty when layouts is not: there is always a best available choice, even for a deck that overruns everywhere.

Parameters:
Return type:

list[tuple[object, FitReport]]

md2star.pptx_fit.reading_order(metrics, layout)[source]

Does the title come before the body, the way the slide will be read?

Left-to-right, top-to-bottom. A layout whose body box sits above its title is a legitimate design – a kicker over a headline, say – but it is the wrong host for content whose heading is the entry point.

Parameters:
Return type:

tuple[float, str]

md2star.pptx_fit.summarise_for_prompt(metrics)[source]

One line of measured facts about a slide, for an LLM prompt.

The selector previously described a slide as “features: table, bullets”, which says what kind of content it is but nothing about how much — and the amount is what decides whether a layout can hold it.

Parameters:

metrics (ContentMetrics)

Return type:

str

md2star.pptx_fit.thirds(metrics, layout, canvas)[source]

Does the focal element land near a rule-of-thirds line?

Only asked about the picture, and only when there is one: the rule is about where the eye goes first, and on an image-forward slide that is the image. Dead-centre is not penalised – it is a deliberate choice often enough – so this is a bonus for hitting a third, never a penalty for missing it.

Parameters:
Return type:

tuple[float, str]