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 (recommended)
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:
- Detect all PGS subtitle tracks in the file
- Auto-detect whether each track is SDR or HDR (per-track color space detection), flagging any mismatch with the video stream's dynamic range
- Prompt you to pick which tracks to process (with an option to validate sparse tracks)
- Prompt you for how many subtitles to decode (defaults to up to 10 cached analysis samples for instant output)
- 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 tocomparefor 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 asvalidatebut 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. A brightest-channel value below 0.55 is treated as insufficient HDR evidence. If a track opens this dark, HDR detection waits for a highlight between 0.55 and 0.65 to settle for at least 500 ms of subtitle timestamps, with a maximum variation of 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.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| proofpgs-1.7.4-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details for proofpgs-1.7.4-py3-none-win_amd64.whl |
| proofpgs-1.7.4-py3-none-manylinux_2_17_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details for proofpgs-1.7.4-py3-none-manylinux_2_17_x86_64.whl |
| proofpgs-1.7.4-py3-none-macosx_11_0_x86_64.whl | Python 3 | none | macOS 11.0+ x86-64 | Details for proofpgs-1.7.4-py3-none-macosx_11_0_x86_64.whl |
| proofpgs-1.7.4-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details for proofpgs-1.7.4-py3-none-macosx_11_0_arm64.whl |
Total release size: 2.4 MB
Release files / proofpgs-1.7.4-py3-none-win_amd64.whl
| Download URL | proofpgs-1.7.4-py3-none-win_amd64.whl |
|---|---|
| Size | 500.7 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
1b2d35c3de37d69745f01bc28185ed8c2fec350eeac76dc383ca26843c396e7d
|
|
BLAKE2b-256 checksum How to use checksums |
068a850780e00339887e7670a53000683cc0e8f76c66ec5ef6880074c5c773fa
|
| 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 logRelease files / proofpgs-1.7.4-py3-none-manylinux_2_17_x86_64.whl
| Download URL | proofpgs-1.7.4-py3-none-manylinux_2_17_x86_64.whl |
|---|---|
| Size | 706.0 kB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
8d8be3aa92f4120ebd4ba45e420295dfc09f831611faa421fd1564bfa11f7289
|
|
BLAKE2b-256 checksum How to use checksums |
4b265ef71d629f3a08e21fb83a94b4b766669bc6bd2de470a3001255bd712b60
|
| 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 logRelease files / proofpgs-1.7.4-py3-none-macosx_11_0_x86_64.whl
| Download URL | proofpgs-1.7.4-py3-none-macosx_11_0_x86_64.whl |
|---|---|
| Size | 620.4 kB |
| Tags | Python 3 macOS 11.0+ x86-64 |
|
SHA-256 checksum How to use checksums |
d97c0100424529e8422bafc59262cfa8c6266ba3c1ade4a3c2deb0acd9284c91
|
|
BLAKE2b-256 checksum How to use checksums |
fd18a3b55a112965d154d7b31a1e7beb7ba5abd012598cd34fa8086ec9df7c8d
|
| 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 logRelease files / proofpgs-1.7.4-py3-none-macosx_11_0_arm64.whl
| Download URL | proofpgs-1.7.4-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 613.9 kB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
4c82b90298e49774feb145f78c9e270d3fcd76cd3f6e47dde8da5da28cc10da4
|
|
BLAKE2b-256 checksum How to use checksums |
109ac9ab969a3a56410bc0c8fd7833eb8fa0d36abf8674bec2afb6b2da063a09
|
| 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 logSecurity for proofpgs 1.7.4
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.
Attestation for proofpgs-1.7.4-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 logA 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.