Because your real interns have better things to do than align your ppt boxes.
intern is a linter for PowerPoint files. Point it at a .pptx and it tells you exactly what's wrong - misaligned boxes, inconsistent fonts, sloppy text, duplicate titles. It can automatically fix alignment, font-size, and whitespace issues.
Existing tools are proprietary Office add-ins or AI-powered web uploads. intern is the first open-source, rule-based CLI linter for PowerPoint - configurable, scriptable, and CI-friendly.
$ intern check deck.pptx
┌───────┬──────────────────────┬─────────┬────────────────┬──────┬───────────────────────────────────┬─────────────────────────────────────────┐
│ Slide ┆ Rule ┆ Type ┆ Position ┆ Id ┆ Text ┆ Message │
╞═══════╪══════════════════════╪═════════╪════════════════╪══════╪═══════════════════════════════════╪═════════════════════════════════════════╡
│ - ┆ FONT_SIZE_VARIETY ┆ - ┆ - ┆ - ┆ - ┆ 8 distinct body font sizes (limit: 3) │
│ ┆ ┆ ┆ ┆ ┆ ┆ │
│ 2 ┆ BULLET_LENGTH ┆ Body ┆ (40px, 132px) ┆ 5 ┆ Our goals for the next quarter... ┆ bullet is 26 words (20-word limit) │
│ 2 ┆ RIGHT_MARGIN ┆ Body ┆ (40px, 132px) ┆ 5 ┆ Our goals for the next quarter... ┆ right edge at 905.4px (typical 927.0px) │
│ ┆ ┆ ┆ ┆ ┆ ┆ │
│ 4 ┆ TITLE_TRAILING_PUNCT ┆ Title ┆ (28px, 15px) ┆ 12 ┆ Project Status. ┆ title ends with '.' - remove it │
│ ┆ ┆ ┆ ┆ ┆ ┆ │
│ 8 ┆ DUPLICATE_TITLE ┆ Title ┆ (28px, 15px) ┆ 44 ┆ Overview ┆ same title as slide 6 │
└───────┴──────────────────────┴─────────┴────────────────┴──────┴───────────────────────────────────┴─────────────────────────────────────────┘
5 violation(s) (5 error, 0 warning)
Pick whichever fits your setup - all three give you the same intern binary.
brew install markusz/intern/internNo toolchain required. Find your target in the table, then run the install commands.
| Platform | Target |
|---|---|
| macOS (Apple Silicon) | aarch64-apple-darwin |
| macOS (Intel) | x86_64-apple-darwin |
| Linux (x86-64) | x86_64-unknown-linux-gnu |
| Windows (x86-64) | x86_64-pc-windows-msvc |
macOS / Linux - set TARGET to your row, then run:
TARGET=aarch64-apple-darwin # ← replace with your target from the table
curl -L https://github.com/markusz/intern/releases/latest/download/intern-$TARGET.tar.gz | tar xz
sudo mv intern /usr/local/bin/Windows - download intern-x86_64-pc-windows-msvc.zip, unzip, and add intern.exe to your PATH.
Requires Rust.
cargo install --path internintern deck.pptx # check (the default action)
intern check slides/ # check every .pptx in a folder
intern fix deck.pptx # auto-fix violations in place
intern ignore deck.pptx -s 3 -r RULE [-e 42] # suppress a violation in speaker notesThat's it. No configuration required to get started - but for ongoing use, most
teams keep an .intern.toml with their preferred thresholds and rule set (see
Config file below).
| Flag | Default | Description |
|---|---|---|
--rules RULE_ID,... |
all | Run only the specified rules |
--disable RULE_ID,... |
none | Skip specific rules |
--threshold <px> |
2 |
Alignment tolerance in pixels |
--slide <n> |
all | Analyze only slide n |
--output table|text|json |
table |
Output format |
--group-by slide|rule |
slide |
Group violations |
--config <path> |
auto | Load settings from a specific config file |
intern check deck.pptx --output json > violations.jsonExit code is 0 when clean (or only warnings) and 1 when an error-severity violation is found - standard for shell scripting and CI pipelines.
Settings can live in a TOML file. intern loads the first one it finds:
- the path passed to
--config <file> ./.intern.tomlin the current directory (project config)~/.config/intern.toml(user config, honours$XDG_CONFIG_HOME)
Files are not merged - the highest-precedence file wins as a whole, and CLI flags override individual settings on top of it.
threshold_px = 2
disable = ["ALL_CAPS"] # turn rules off in bulk
# only = ["TITLE_Y", "TITLE_X_WIDTH"] # if set, ONLY these rules run
[output]
format = "table"
group_by = "rule"
[rules.TITLE_LENGTH]
max_words = 8
[rules.ALL_CAPS]
severity = "warning" # report it, but don't fail CI
[rules.SLIDE_COUNT]
enabled = true # SLIDE_COUNT is off by default; enable it explicitly
max_slides = 40Each rule is configured in its own [rules.<RULE_ID>] table; disable and only
are blunt top-level lists. See the documentation for the full reference.
The quickest way to suppress a violation is intern ignore:
intern ignore deck.pptx -s <slide> -r <rule> # whole slide
intern ignore deck.pptx -s <slide> -r <rule> -e <id> # one elementThis writes an intern: disable line into that slide's speaker notes and backs
the file up to deck.pptx.bak. The slide number and element id come straight from
the violation table output.
You can also edit the notes by hand. To skip an entire slide:
intern: disable # skip every rule on this slide
intern: disable TITLE_Y, DUPLICATE_TITLE # skip only these rules
To suppress a rule for one element only (use the id shown in the Id column):
intern: disable(42) EMPTY_TEXTBOX
intern: disable(42) # suppress every rule for that element
The slide is dropped before those rules run, so it skews no baselines (like the median title position) either.
29 rules across four categories. For each rule: what it does, why it matters, defaults, and examples - see RULES.md.
Rules marked off require enabled = true in [rules.<RULE_ID>] or
--rules <ID> to run. Any on-by-default rule can be suppressed with --disable.
| Rule | Default | What it catches |
|---|---|---|
LEFT_MARGIN |
on | Slide's leftmost unit is off the typical left margin |
RIGHT_MARGIN |
on | Slide's rightmost unit right edge is off the typical right margin |
BOTTOM_MARGIN |
on | Content extends deeper than the typical bottom margin |
TITLE_MARGIN |
on | Gap between title and nearest content unit differs from the typical gap |
CLOSE_X |
on | Two units have X positions within threshold - likely misaligned |
CLOSE_Y |
on | Two units have Y positions within threshold - likely misaligned |
TITLE_Y |
on | Title top-edge inconsistent across slides |
TITLE_X_WIDTH |
on | Title left-edge or width inconsistent across slides |
TEXT_ELEMENT_OVERLAP |
on | Two text-bearing elements on the same slide have overlapping rects |
ELEMENT_OVERFLOW |
on | Element extends outside the slide bounds |
| Rule | Default | What it catches |
|---|---|---|
TITLE_FONT_SIZE |
on | Title font size differs from the majority |
FONT_SIZE_VARIETY |
on | Too many distinct body font sizes across the deck |
BODY_FONT_FAMILY |
on | Body font family differs from the majority across slides |
FONT_VARIETY |
on | Too many distinct font families across the deck |
COLOR_VARIETY |
on | Too many distinct text colors across the deck |
BODY_TEXT_COLOR |
off | Body text color differs from the majority across slides |
| Rule | Default | What it catches |
|---|---|---|
DOUBLE_SPACE |
on | Paragraph contains two or more consecutive spaces |
LEADING_SPACE |
on | Paragraph starts with whitespace |
REPEATED_WORD |
on | Two consecutive identical words ("the the") |
BULLET_CAPITALIZATION |
on | Bullets have inconsistent first-letter capitalization |
BULLET_PUNCTUATION |
on | Bullet ending punctuation is inconsistent across the deck |
BULLET_LENGTH |
on | Bullet is too long |
ALL_CAPS |
off | Paragraph text is ALL CAPS |
| Rule | Default | What it catches |
|---|---|---|
TITLE_LENGTH |
on | Title is too long |
TITLE_TRAILING_PUNCT |
on | Title ends with . , : or ; |
DUPLICATE_TITLE |
on | Title text is duplicated on another slide |
EMPTY_TEXTBOX |
on | Text box has no text content |
TITLE_PRESENT |
off | Slide has no title element |
SLIDE_COUNT |
off | Deck has too many slides |
intern-core is the engine without the CLI - use it to build custom tooling, reporting pipelines, or editor integrations.
[dependencies]
intern-core = { git = "https://github.com/markusz/intern" }use intern_core::{
model::EMU_PER_PX,
reader::read_presentation,
rules::{all_rules, Limits, RuleContext},
};
let pres = read_presentation("deck.pptx")?;
let ctx = RuleContext {
threshold: 2 * EMU_PER_PX,
slide_width: pres.slide_width,
slide_height: pres.slide_height,
};
let limits = Limits { slide_count: 30, ..Limits::default() };
let violations: Vec<_> = all_rules(&limits)
.iter()
.flat_map(|r| r.check(&pres.slides, &ctx))
.collect();