# Screen Recording To PDF

> Screen Recording to PDF

- Skill: `denlie-code/screen-recording-to-pdf` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add denlie-code/screen-recording-to-pdf`
- Raw SKILL.md: https://api.skillmd.com/api/skills/denlie-code/screen-recording-to-pdf/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: Denlie-code (https://skillmd.com/u/denlie-code)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/denlie-code/screen-recording-to-pdf

---


# Screen Recording to PDF

Turn a screen recording (or any video) into a multi-page PDF. **Pick the mode
by what the recording IS:**

- **Scrolling / continuous content** (a chat-history recording, a long page you
  scrolled through, a feed) where **every item must be captured** -> use
  **`--capture-all`**. It measures the scroll and keeps a frame each half-screen
  so nothing scrolls past uncaught.
- **Demonstrations / tutorials / click-throughs** where you only want the
  *moments that change* -> use **`--auto`** (or manual `--scene-threshold` /
  `--max-frames`).

Two scripts, pipelined:

```
video.mp4 --extract_frames.py--> frame_*.jpg --frames_to_pdf.py--> output.pdf
```

`extract_frames.py` is adapted from [mugnimaestra/video-frames-skill](https://github.com/mugnimaestra/video-frames-skill)
(see `references/video-frames-source.md`). Local additions: an ffmpeg/ffprobe
resolution layer (no system ffmpeg needed), a fix to the scene-detection filter
for ffmpeg >= 7, `--auto` (smart key-frame + pause de-dup), and `--capture-all`
(scroll-aware complete capture).

## Prerequisites

```bash
python -c "import imageio_ffmpeg, PIL, numpy; print('core OK')"
python -c "import img2pdf; print('img2pdf OK (lossless)')" || echo "optional"
```

Install anything missing (one line, no admin / no system ffmpeg):

```bash
pip install imageio-ffmpeg Pillow numpy img2pdf
```

`numpy` is required **only** for `--capture-all` (scroll matching). The other
modes work without it.

## Quick Start

**Chat-history / scrolling recording (capture every message):**

```bash
python scripts/extract_frames.py chat.mp4 --capture-all \
  | python scripts/frames_to_pdf.py --json - -o chat.pdf
```

**Demo / tutorial (key frames only):**

```bash
python scripts/extract_frames.py demo.mp4 --auto \
  | python scripts/frames_to_pdf.py --json - -o demo.pdf
```

## Workflow

1. **Get the video path** and confirm it exists.
2. **Pick the mode** by content (see Decision Guide). If unsure whether it's
   "scrolling chat" vs "demo", ask the user, or default to `--capture-all` when
   completeness matters.
3. **Extract frames** with `extract_frames.py` -> JSON to stdout (`frames`,
   `output_dir`, `resolution`, `summary`, plus `strategy`/`capture`/`dedup`).
4. **Assemble the PDF** with `frames_to_pdf.py` (frames dir or piped JSON).
5. **Report** output path, page count, size. For `--capture-all`, mention the
   `capture.kept` count and that adjacent pages overlap (~50%+) so every
   message is on multiple pages.
6. **Clean up** the temp frames dir if throwaway.

## Decision Guide

| Mode | When to use | Example |
|------|-------------|---------|
| **`--capture-all`** ⭐ | **Scrolling / continuous content** -- chat-history recording, long-page scroll, feed. Captures EVERY message (no skips). Measures scroll, keeps a frame each half-screen. | `--capture-all` |
| **`--auto`** ⭐ | **Demos / tutorials / click-throughs** -- wants only the moments that change; de-duplicates pauses. | `--auto` |
| **`--scene-threshold T`** | Manual scene detection (distinct screen changes). | `--scene-threshold 0.4` |
| **`--max-frames N`** | Predictable page count, smooth/gradual content. | `--max-frames 30` |
| **`--fps N`** | Fixed-rate sampling. Rarely best. | `--fps 1` |

All modes are mutually exclusive at the CLI (`--capture-all` overrides the
others).

### `--capture-all` tuning

| Option | Default | Meaning |
|--------|---------|---------|
| `--sample-fps FPS` | `10.0` | Dense sampling rate. Must be high enough that no message scrolls all the way past between samples. Raise for fast scrolls. |
| `--overlap FRAC` | `0.5` | Keep a frame each time content scrolls by this fraction of the frame height. **Lower = more pages / safer; higher = fewer pages.** 0.5 = adjacent pages overlap ~50%. |

How it guarantees completeness: it densely samples (default 10fps), measures the
vertical scroll between consecutive frames, and only advances to a new page once
the content has scrolled by `overlap` of the screen. Adjacent pages therefore
always overlap, so any message that was ever on screen appears on at least one
(usually several) pages. For a 133s phone chat scroll this yields ~50 pages with
~79% adjacent overlap. If you see gaps, lower `--overlap` (e.g. 0.35) or raise
`--sample-fps`.

### `--auto` tuning

| Option | Default | Meaning |
|--------|---------|---------|
| `--target-density SECS` | `2.0` | Target seconds between kept frames (auto-raised for long videos). |
| `--dedup-threshold DIFF` | `6.0` | Grayscale mean-abs-diff below which two frames are duplicates (higher = keep more). |

### Quality presets

| Preset | Max dim | JPEG q | Best for |
|--------|---------|--------|----------|
| `balanced` | 1024px | 3 | General (default) |
| `detailed` | 1568px | 2 | UI detail, small text, color fidelity |
| `ocr` | 1568px | 1 (gray+sharpen) | Text-heavy -- **loses color** |

## PDF Options (`frames_to_pdf.py`)

| Option | Default | Meaning |
|--------|---------|---------|
| `<frames_dir>` | -- | Directory of frame images |
| `--json FILE` | -- | Read frame list from extract JSON (`-` = stdin) |
| `-o / --output` | required | Output PDF path |
| `--page` | `none` | `a4`/`a4l`/`letter`/`letterl`/`none` (`none` = native pixel size) |
| `--margin` | `0` | Page margin in points (needs `--page`) |
| `--backend` | `auto` | `img2pdf` (lossless JPEG embed) / `pillow` / `auto` |

## Troubleshooting

- **Chat recording is missing messages.** You used a key-frame mode (`--auto` /
  `--max-frames`) -- those skip frames and miss messages that scroll past. Use
  **`--capture-all`** instead. If still gappy, lower `--overlap 0.35` and/or
  raise `--sample-fps 15`.
- **`--capture-all` page count too high.** Raise `--overlap` (e.g. `0.65` =
  fewer pages, less overlap) -- but stay below ~0.8 or you risk gaps on fast
  scrolls.
- **`--capture-all` says it needs numpy.** `pip install numpy`.
- **Scene detection returns 0 frames.** Normal for smooth scrolls -- use
  `--capture-all` (chat) or `--auto` (it falls back to fixed-rate).
- **`ffmpeg not found`.** `pip install imageio-ffmpeg` (bundled, no admin) or
  `winget install Gyan.FFmpeg`.
- **Text blurry.** `--preset ocr` (grayscale, sharpened) or `--preset detailed
  --max-dimension 1920`.
- **No `img2pdf`.** Falls back to Pillow (re-encodes). `pip install img2pdf`.

## Files

- `scripts/extract_frames.py` -- video -> JPEG frames. Modes: `--capture-all`
  (scroll-aware complete capture), `--auto` (smart key-frames + pause dedup),
  `--scene-threshold`/`--max-frames`/`--fps` (manual). Prints JSON.
- `scripts/frames_to_pdf.py` -- frames -> one PDF (img2pdf lossless embed, or Pillow).
- `references/video-frames-source.md`, `references/llm-image-specs.md` -- upstream refs.
- `references/THIRD-PARTY-LICENSES.md` -- full third-party license texts.
- `LICENSE` -- Mixed license (WDenlie Commercial-Authorization for original work + MIT for upstream-derived).

## License & Third-Party Notices

This skill uses a **mixed license** (see [LICENSE](LICENSE) for the full text):

- **Original work** — `frames_to_pdf.py`, the `--capture-all` / `--auto` /
  pause-de-dup / ffmpeg-resolution additions in `extract_frames.py`, and
  `SKILL.md` — is © 2026 **WDenlie** under the **PolyForm Noncommercial License
  1.0.0**: free for personal / academic / non-commercial use **with attribution**;
  **commercial use requires a separate license**. To license commercially,
  contact **WDenlie via WeChat (ID: WDenlie)**.
- **Upstream-derived code** — the core frame-extraction logic in
  `extract_frames.py`, plus `references/video-frames-source.md` and
  `references/llm-image-specs.md` — is adapted/copied from
  [mugnimaestra/video-frames-skill](https://github.com/mugnimaestra/video-frames-skill)
  and remains under its **MIT License** (MIT permits commercial use of those
  portions; this cannot be revoked).

**No binaries, no vendored libraries** are shipped; deps are pip-installed by
the user and ffmpeg is invoked only as an external process.

- **Runtime deps** (not bundled): `img2pdf` (LGPL-3.0-or-later),
  `imageio-ffmpeg` (BSD-2-Clause), `Pillow` (PIL Software License), `numpy`
  (BSD-3-Clause, only for `--capture-all`).
- **ffmpeg** is the GPL (`--enable-gpl`) build when fetched by `imageio-ffmpeg`,
  invoked **only as a separate subprocess** -- the skill does not link, bundle,
  or redistribute it, so GPL copyleft does not reach this skill's code. For
  commercial distribution prefer an LGPL-only ffmpeg build (configure without
  `--enable-gpl`/x264/x265) -- this skill only decodes video and encodes JPEG.

Full third-party license texts: [references/THIRD-PARTY-LICENSES.md](references/THIRD-PARTY-LICENSES.md).

> *Not legal advice.* Engineering-level license guidance; have counsel review
> before commercial distribution.

*Claude, OpenAI, and Gemini are trademarks of their respective owners; this
skill is independent and not affiliated with or endorsed by them.*

