Skip to main content
ProofPGS icon

ProofPGS

A tool for inspecting and exporting PGS (Presentation Graphic Stream) subtitles. Validates PGS tracks with per-track SDR/HDR detection and can export each subtitle as a PNG using the correct colour pipeline, either HDR (UHD Blu-ray, BT.2020 + PQ) or SDR (standard Blu-ray, BT.709).

Accepts .sup files directly, or video containers (MKV, MK3D, M2TS) from which PGS subtitle tracks are automatically discovered and extracted via libpgs.

Installation

pip install proofpgs

This installs ProofPGS with all dependencies and a bundled libpgs binary. The proofpgs command is available system-wide after installation.

Available for Windows x64, Linux x64, macOS x64, and macOS ARM64.

Release archive

Alternatively, download a pre-built archive for your platform from the Releases page. Each archive includes ProofPGS and a pre-built libpgs binary. Install the Python dependencies manually:

pip install numpy pillow

Running from source

If you prefer to run from a git clone, you'll need to provide the libpgs binary yourself. Place libpgs (or libpgs.exe on Windows) in proofpgs/bin/, or add it to your system PATH. Then install dependencies with pip install numpy pillow.

Optional dependency

  • FFmpeg, needed only for the video stream dynamic range mismatch badge. If ffprobe is not on PATH, this feature is silently skipped.

Quick Start

Point ProofPGS at any video file or .sup file:

proofpgs movie.mkv

That's it. ProofPGS will:

  1. Detect all PGS subtitle tracks in the file
  2. Auto-detect whether each track is SDR or HDR (per-track color space detection), flagging any mismatch with the video stream's dynamic range
  3. Prompt you to pick which tracks to process (with an option to validate sparse tracks)
  4. Prompt you for how many subtitles to decode (defaults to up to 10 cached analysis samples for instant output)
  5. Decode using the correct color pipeline and save PNGs to a <filename>_pgs_output/ folder next to the input file

Works the same way with .sup and .m2ts files.

Advanced Usage

proofpgs <input_file> [options]

Skip the prompts (non-interactive)

# Specific tracks, first 20 subtitles:
proofpgs movie.mkv --tracks 1,3 --first 20

# All tracks, all subtitles:
proofpgs movie.mkv --tracks all

# Custom output directory:
proofpgs movie.mkv --out ./my_output

Extract a specific time range

Use --start and --end to extract subtitles from a specific portion of the file. libpgs seeks directly to the target offset, so data before the start point is not read.

# Subtitles from 5 minutes onward:
proofpgs movie.mkv --start 0:05:00

# Subtitles between 1:30:00 and 1:35:00:
proofpgs movie.mkv --start 1:30:00 --end 1:35:00

# First 10 subtitles within a time window:
proofpgs movie.mkv --start 0:05:00 --end 0:10:00 --first 10

Timestamps accept HH:MM:SS.ms, MM:SS.ms, SS.ms, or plain seconds (e.g. 300). --start and --end can be used independently or together, and compose with --first.

Output modes

ProofPGS has six output modes:

  • auto (default): automatically detects whether each subtitle track was mastered for SDR or HDR by analyzing palette data, then decodes each track with the correct pipeline independently. A container with mixed SDR and HDR tracks will process each track using its own detected color space. Falls back to compare for any individual track where detection is inconclusive.
  • compare: for delivery proofing. Produces an annotated PNG with a dark background showing the SDR and HDR decodes side by side, labelled for easy comparison. These are opaque RGB images meant for visual review.
  • hdr: direct export. Outputs the HDR (BT.2020+PQ) decode as a transparent PNG, cropped to content. Useful when you need the subtitle graphic itself.
  • sdr: direct export. Outputs the SDR (BT.709) decode as a transparent PNG, cropped to content.
  • validate: analyzes all tracks without a time limit (with scan progress) and displays track information and SDR/HDR detection results without producing any output. Useful for thoroughly checking what PGS tracks a file contains and whether they are mastered for SDR or HDR, including sparse tracks that may be skipped during normal interactive analysis.
  • validate-fast: runs the same analysis as validate but under the normal 10-second wallclock budget. Sparse tracks that can't be analyzed in time are flagged, and you're prompted to re-analyze them without a time limit if desired. Useful for a quick check when a full unbounded scan isn't needed.
# Auto-detect color space and decode accordingly (default):
proofpgs input.sup

# Force side-by-side comparison:
proofpgs input.sup --mode compare

# Direct export, transparent HDR-decoded PNGs:
proofpgs input.sup --mode hdr

# Direct export, transparent SDR-decoded PNGs:
proofpgs input.sup --mode sdr

# Show track info and detection only (no output):
proofpgs movie.mkv --mode validate

# Quick validation under 10s budget (prompts to re-analyze sparse tracks):
proofpgs movie.mkv --mode validate-fast

Options

Option Values Default Description
--mode auto, compare, hdr, sdr, validate, validate-fast auto auto detects color space per-track and decodes each track independently with the correct pipeline. compare produces annotated side-by-side proofing images. hdr and sdr produce direct transparent PNG exports. validate shows track info and detection only (no output). validate-fast same as validate but under the 10s analysis budget with option to re-analyze sparse tracks.
--tonemap clip, reinhard clip HDR-to-SDR tonemapping strategy. clip limits the peak RGB channel at 203 nits reference white. reinhard applies a soft roll-off to the peak. Both scale all three channels together to preserve linear RGB ratios.
--out path <filename>_pgs_output/ next to input file Output directory.
--first integer all Decode only the first N subtitle display sets.
--start timestamp beginning Start timestamp for extraction (e.g. 0:05:00, 5:00, 300). Seeks directly to the target offset.
--end timestamp end of file End timestamp for extraction (e.g. 0:10:00, 10:00, 600).
--tracks e.g. 1,3,4 or all interactive Which PGS tracks to process (1-based, container input only).
--nocrop flag off Output full video-frame-sized PNGs instead of cropping to subtitle content.
--threads integer auto (up to 8) Number of parallel rendering threads.
--install flag n/a Register file manager context menu entries for all supported file types.
--uninstall flag n/a Remove file manager context menu entries.

File Manager Integration

ProofPGS can add a right-click context menu for all supported file types (.sup, .mkv, .mk3d, .m2ts). The menu shows entries for each output mode, filtered by file type.

# Register context menu entries:
proofpgs --install

# Remove context menu entries:
proofpgs --uninstall

When installed via pip, the context menu invokes the proofpgs command directly. When running from source or a release archive, the install command records the paths to both the Python interpreter and the project directory. If you move the project or switch Python environments, run --install again to update the paths.

Platform Mechanism Notes
Windows Explorer context menu via registry (HKCU) On Windows 11, right-click and choose Show more options to see the submenu.
Linux Freedesktop .desktop files in ~/.local/share/applications/ Entries appear in the Open With menu. A custom MIME type is registered for .sup files.
macOS Finder Quick Actions via Automator .workflow bundles in ~/Library/Services/ You may need to enable the actions in System Settings > Privacy & Security > Extensions > Finder.

Output

Each subtitle is saved as a PNG file named with its display set index, timestamp, and decoded color space:

movie_pgs_output/
  track_1_eng/
    ds_0000_12500ms_sdr.png
    ds_0001_15200ms_sdr.png
    ...
  track_2_ger_forced/
    ds_0000_8300ms_hdr.png
    ...

The output folder is named after the input file (e.g. movie.mkv becomes movie_pgs_output/). The range suffix (_sdr, _hdr, or _compare) indicates which color pipeline was used to decode the subtitle.

For .sup input (single track), images are written directly to the output directory without a track subfolder.

How SDR/HDR Detection Works

ProofPGS classifies each track by analysing the subtitle's own palette colours, not the video stream's metadata. Subtitle tracks can come from different sources than the video, so the video's range is not a reliable signal. The core check is a PQ plausibility test. Every visible palette entry is decoded under the HDR (BT.2020 + PQ) interpretation, where the 0-1 colour range maps onto 0-10,000 nits. Real HDR subtitles are authored near the 203-nit reference white, so if the brightest entry implies more than about 1000 nits under PQ, the HDR interpretation is implausible and the track is SDR. This is especially decisive for coloured text, where SDR gold at Y=178 decodes to a value that would mean about 4800 nits under PQ, obviously not real HDR.

When the PQ test is ambiguous, ProofPGS falls back to Y-value thresholds (SDR white sits at Y=235, HDR reference white at Y=143) and prefers achromatic entries when available, since white/gray decodes the same under either colour matrix. Two filters keep the detector honest: invisible entries (alpha below roughly 12.5%) are skipped to ignore fade-in palettes, and "ghost" palette entries that no pixel references are excluded so a single unrendered entry can't flip the verdict. Detection runs per track, so a container with mixed SDR and HDR subtitles decodes each one with the correct pipeline. Only genuinely inconclusive tracks fall back to compare mode.

Dark, opaque graphics need extra care: 3D subtitles can fade through black by changing their palette colours without reducing alpha. The brightest channel must reach roughly 150 nits under the PQ interpretation (code value 0.55), either in source BT.2020 or after linear conversion to BT.709. Checking both prevents saturated colours from being mistaken for dark graphics; this check precedes tone mapping and alpha blending. If a track opens below this intensity, HDR detection waits for a highlight above that floor, with a source PQ peak no higher than 0.65, to settle for at least 500 ms of subtitle timestamps. Its source PQ peak may vary by at most 0.015. Strong SDR evidence still ends analysis immediately, and normally lit openings retain single-sample detection. This is a heuristic: permanently dim SDR can resemble HDR. Tracks without enough evidence remain unknown and use comparison output in auto mode; the existing analysis limits still apply.

Detection and colour regression tests can be run with python -m unittest discover -s tests.

Colour Pipeline

HDR (UHD Blu-ray)

The palette is encoded in BT.2020 primaries with ST 2084 (PQ) transfer function, per the UHD BD specification. ProofPGS applies the full inverse pipeline:

BT.2020 YCbCr (limited range)  ->  BT.2020 matrix  ->  PQ EOTF (linearise)
  ->  BT.2020 to BT.709 gamut mapping  ->  Tonemap to SDR
  ->  sRGB gamma  ->  PNG

HDR colours are fitted into the SDR range by scaling all three linear RGB channels together. In clip mode, colours already within range stay unchanged; brighter colours are scaled until their peak channel reaches 203 nits reference white. This avoids shifting bright orange toward yellow by clipping red independently. reinhard uses a shared soft roll-off instead. Neutral whites follow the same tone curves as before, and alpha is preserved.

This trades highlight brightness for preservation of linear RGB ratios. It does not reproduce the full HDR appearance on an SDR display: colours outside the BT.709 primaries still have negative channels clamped to zero and can change chromaticity.

SDR (standard Blu-ray)

BT.709 YCbCr (limited range)  ->  BT.709 matrix  ->  BT.1886 linearise (gamma 2.4)
  ->  sRGB gamma  ->  PNG

Performance

Track detection is incremental: each new display set is inspected once, while colour statistics and brightness-settling state are retained. Extraction restarts reuse that progress and skip replayed samples before bitmap decoding. The discovery stream is reused for initial analysis, including indexed containers. These optimizations retain the same detection thresholds and earliest conclusive sample.

All file I/O is handled by libpgs, a Rust tool built for PGS segment extraction. libpgs streams decoded display sets over a subprocess pipe, with no temp files and no intermediate formats. For MKV files with a Cues index, libpgs seeks directly to subtitle data, reading only a few MB out of tens of GB for large UHD remuxes.

PNG rendering uses multiple threads by default (up to 8, override with --threads).

Palette indices are expanded with Pillow's native converter. Crop analysis scans only the occupied image region, retaining the same padding and thin-artifact filtering. Comparison exports reuse pre-rendered labels and branding across images; timestamps are drawn separately. PNG compression settings and rendered pixels are unchanged by these optimizations.

Project Structure

proofpgs/
  assets/             # Bundled resources (fonts, icons)
    Sora-Medium.ttf
    Sora-Regular.ttf
  bin/                # Bundled libpgs binary (platform-specific, gitignored)
  __init__.py         # Public API exports
  __main__.py         # python -m proofpgs entry point
  cli.py              # Argument parsing and main()
  constants.py        # PQ constants, segment types, file extensions
  detect.py           # SDR/HDR auto-detection via PQ plausibility analysis
  parser.py           # Display set content check (ds_has_content)
  color.py            # Colour-space math and palette decoding (HDR & SDR)
  renderer.py         # Display set rendering and PNG output
  libpgs.py           # libpgs CLI adapter (subprocess streaming)
  ffmpeg.py           # ffprobe video range detection
  interactive.py      # Interactive track and count selection
  pipeline.py         # High-level orchestration
  shellmenu.py        # File manager context menu integration (Windows, Linux, macOS)
  style.py            # Terminal styling and color output
LICENSES/
  OFL.txt             # SIL Open Font License 1.1 (Sora font)

Build Provenance

Before publishing, let the Tests workflow pass and manually run the Release workflow from GitHub Actions. A manual run builds and validates all four platform wheels without publishing to PyPI or uploading release assets. Publishing a GitHub Release triggers the full release flow; its tag must match the package version (for example, v1.7.4). Each platform runs the regression tests and validates wheel metadata before publication.

Release archives are built entirely in GitHub Actions from auditable source code, so no locally-built binaries are uploaded. The libpgs binary included in each release is compiled from the libpgs source at its latest tagged release using cargo build --release on each platform's native CI runner.

Every release archive includes a BUILD_INFO.txt with the exact libpgs tag, commit hash, build target, and a link to the workflow run log. Release artifacts are signed with Sigstore artifact attestations, cryptographically linking each archive to the GitHub Actions workflow and source commit that produced it.

To verify a downloaded release:

gh attestation verify ProofPGS-<version>-<platform>.zip --repo matthane/ProofPGS

Metadata

Release files for proofpgs 1.7.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for proofpgs 1.7.5
File Actions
proofpgs-1.7.5-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details for proofpgs-1.7.5-py3-none-win_amd64.whl
proofpgs-1.7.5-py3-none-manylinux_2_17_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details for proofpgs-1.7.5-py3-none-manylinux_2_17_x86_64.whl
proofpgs-1.7.5-py3-none-macosx_11_0_x86_64.whl Python 3 none macOS 11.0+ x86-64 Details for proofpgs-1.7.5-py3-none-macosx_11_0_x86_64.whl
proofpgs-1.7.5-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details for proofpgs-1.7.5-py3-none-macosx_11_0_arm64.whl

Total release size: 2.4 MB

Release files / proofpgs-1.7.5-py3-none-win_amd64.whl

Download URL proofpgs-1.7.5-py3-none-win_amd64.whl
Size 501.2 kB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
a17ced0b7ff64e8330fa5ec41a9a457a6a54bb0d290c5f3ae76a8e179ef862d7
BLAKE2b-256 checksum
How to use checksums
7e3ee77953a90ba86ab9fd4ac0e05a5664d97ffe4ab221bd22eb890ebfe83d35
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / proofpgs-1.7.5-py3-none-manylinux_2_17_x86_64.whl

Download URL proofpgs-1.7.5-py3-none-manylinux_2_17_x86_64.whl
Size 706.5 kB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
631f1c967c5a227f2a1aaa04d8ebea472cf066ec7e5481674d99e80e80e372e7
BLAKE2b-256 checksum
How to use checksums
9e733f3dbb50866851c5a8606d53a9b195040cf27a5384e7c897b5bef0bd79cf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / proofpgs-1.7.5-py3-none-macosx_11_0_x86_64.whl

Download URL proofpgs-1.7.5-py3-none-macosx_11_0_x86_64.whl
Size 620.9 kB
Tags Python 3 macOS 11.0+ x86-64
SHA-256 checksum
How to use checksums
02d1fe41236c8950636b680f7868b9cab38672a190e0b47b10e0aa0b609e0cde
BLAKE2b-256 checksum
How to use checksums
0d81c695f7dccea7ebc1692e6e9b664e332c0de7ad9ff016708f94ccb07b5ad8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / proofpgs-1.7.5-py3-none-macosx_11_0_arm64.whl

Download URL proofpgs-1.7.5-py3-none-macosx_11_0_arm64.whl
Size 614.4 kB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
b3bb026930dce9afbe53647869f1d6855390fb3b1da500844dc03fa091a44b3c
BLAKE2b-256 checksum
How to use checksums
50f98253fced80dcb20126fcbb3b52e426beb722a513ae54987a3368897fe54b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Security for proofpgs 1.7.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 4 files in this release have been uploaded with provenance metadata.

Select from 4 files

Attestation for proofpgs-1.7.5-py3-none-win_amd64.whl

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page