Releases: priyadip/ecompress
Release list
ecompress 2.5.0 - cut and combine PDFs and videos
Install or upgrade:
pip install -U ecompressAdded
-
Cut and combine PDFs and videos. Four commands -
add_pdf,cut_pdf,
add_video,cut_video- share one grammar, so a person or an LLM agent
only has to learn it once:add_pdf "D:/a.pdf"[2-9] "D:/b.pdf"[7-16] "D:/out" cut_pdf "C:/My Documents/book.pdf"[2-5,8-12,20] "D:/out" add_video "a.mp4"[00:00-00:30] "b.mp4"[01:10-02:00] "D:/out" cut_video "movie.mp4"[00:02:10-00:05:30]The folder at the end is optional; without it the result is saved beside
the input under a new name, never replacing a file.The same operations are available from Python as
execute(),cut_pdf(),
add_pdf(),cut_video()andadd_video(), returning anEditResult. -
PDF pages are copied as objects, so nothing is rasterised. Video is
re-encoded at visually lossless quality so cuts are frame-accurate and clips
of different sizes, frame rates and audio layouts can be joined; a clip with
no audio contributes silence to keep sound in sync.cut_video --copycuts
without re-encoding when speed matters more than a frame-exact start. -
Every range is validated before anything is written, and every result is
built in a temporary file and checked with pikepdf or ffprobe before it is
moved into place, so a mistake never leaves a partial file behind. -
CommandSyntaxError, raised with a message naming the offending text and
the expected form.
Notes
- There is deliberately no arrow before the output: every shell treats
>as
output redirection. PowerShell reads"x"[2-9]as string indexing and zsh
globs[...], so in those shells the range goes inside the quotes
("book.pdf[7-16]", which works everywhere) or PowerShell gets--%.
Quotes around paths are optional, because shells strip them anyway. - Probing now reads a video's display rotation, so portrait phone footage keeps
its orientation when joined with other clips.
ecompress 2.4.0
pip install --upgrade ecompressAdded
A progress bar while encoding. Long encodes printed nothing for minutes, so a run was indistinguishable from a hang. FFmpeg's own -progress stream now drives a bar with a percentage, a time estimate, and what is being encoded:
[#####...................] 21% 0:09 left 2560x1440 @ 24 fps
Drawn only to a real terminal, so piping the output or using --quiet / --json stays clean. The estimate is withheld for the first few percent, where start-up cost would make it read wildly wrong.
Fixed
Quality targeting could replace a better result with a worse one. On a 4K screen recording the bitrate search reached 38.3 MB, the CRF fill managed only 34.4 MB - and the fill's result was adopted anyway, delivering the smaller of the two against a 40 MB floor. The fill now reports the size it achieved and is taken only if it improves on what came before.
The CRF ladder stopped at 16, not always enough to fill a window on very compressible 4K; it now runs to 10.
--timeout was not enforced during an encode: reading FFmpeg's progress stream blocks until it exits, so the deadline could only be noticed after the work had finished. A watchdog now stops it on time.
448 tests, 84% coverage, verified on Linux, macOS and Windows across Python 3.10-3.13.
ecompress 2.3.0
pip install --upgrade ecompressFixed
A bitrate proven to work is no longer thrown away when the resolution changes.
Every rung of the ladder restarted its search from the original opening estimate. Having learned that 3.13 Mbps worked at 1440p, the search dropped back to 965 kbps on moving to 2160p - so the higher resolution produced a smaller file than the rung below it:
Attempt 2: 21.0 MB [3.13 Mbps @ 2560x1440] learned 3.13 Mbps works
Attempt 3: 16.2 MB [965 kbps @ 3840x2160] threw it away
The learned rate now carries forward, so each climb makes the file larger, monotonically, and the size floor is reached in fewer attempts.
Also: the climb decision now looks at what the current frame size achieved rather than a global best from another rung, and saturation is judged within a rung.
Changed
Long sources are searched on a 30-second sample. Each search pass had to encode the whole file. Benchmarked: 4K -> 4K runs at 19 fps against 65 fps for 4K -> 1080p, so a five-pass run on a six-minute 4K source could exceed an hour. Above 90 seconds the search now runs on a slice taken a quarter of the way in, and only the winning settings get a full-length encode.
The prediction is never the result - the full encode is measured and validated exactly as before, and corrected if the sample misjudged the file. Quality is unaffected.
Two measurements worth recording, both negative: -hwaccel auto made 4K decoding three times slower, not faster, because every frame is copied back from the GPU; and the scaler choice (lanczos, bicubic, fast_bilinear) makes no measurable difference. Neither is worth doing.
427 tests, 84% coverage, verified on Linux, macOS and Windows across Python 3.10-3.13.
ecompress 2.2.0
pip install --upgrade ecompressFixed
A size floor is now reachable even at source resolution.
2.0.1 taught the search to climb when the encoder could not spend its budget, and 2.1.0 made the opening guess accurate. Both worked through -b:v, an average bitrate target - and libx264 will not pad a stream with bits the content does not call for. On very compressible video it undershoots whatever number it is given, so once the climb reached the source resolution there was nowhere left to go and the result still fell short of the floor.
The search now switches to -crf, which fixes a quality level rather than an average rate, so lowering it always produces more bits. Measured on static but detailed 4K:
ABR 400 kbps -> 280 kbps (saturated, will not spend more)
CRF 24 -> 564 kbps
CRF 16 -> 786 kbps (2.8x)
A clip that previously stopped at 752 KB against a 1.01 MB floor now lands at 1.18 MB, still at full 3840x2160.
Changed
When saturation is detected the climb jumps straight to the source resolution instead of stepping one rung at a time. For content that cannot absorb bits the source is both the largest file and the best quality, so the intermediate rungs cannot help - and skipping them saves several encodes on large sources.
424 tests, 86% coverage, verified on Linux, macOS and Windows across Python 3.10-3.13.
ecompress 2.1.0
pip install --upgrade ecompressChanged
The opening guess is now calibrated from your own file, not a generic constant.
2.0.1 could recover from a bad first guess by climbing, but it still started from an assumption about typical footage - then spent several encodes discovering the assumption was wrong.
Your source file already answers the question. Its bitrate, resolution and frame rate give a measured bits-per-pixel for that specific content, free, before anything is re-encoded. A 4K 62.5 fps screen recording at 3.3 Mbps measures 0.26 against the generic 1.5 - 5.7x more compressible - and the starting plan moves accordingly:
| Starting plan | Pixels per frame | |
|---|---|---|
| Generic constant | 1024x576 @ 31.25 fps | 589,824 |
| Source-calibrated | 2560x1440 @ 24 fps | 3,686,400 (6.2x) |
which is where the climb was heading anyway, reached in one step instead of several.
The measurement can only ever lower the requirement, never raise it: a visually-lossless source shows the content can absorb bits, not that it needs them. Without that cap a high-quality clip would be judged unable to hold its own resolution at 90% of its own bitrate, and would lose frame rate for no reason.
The measurement-driven climb from 2.0.1 is unchanged and still corrects whatever the opening guess gets wrong - it now has far less to correct.
423 tests, 86% coverage, verified on Linux, macOS and Windows across Python 3.10-3.13.
ecompress 2.0.1
pip install --upgrade ecompressFixed
Easily-compressed video no longer settles far below the requested size.
Content with little movement - a screen recording, a slide deck, a talking head on a static background - cannot absorb the bitrate it is given. Past a point the encoder is already at its best quality for that frame size, and more bits change nothing:
Attempt 1: 965 kbps -> 12.4 MB
Attempt 2: 3.13 Mbps -> 12.5 MB
The search only knew how to move down the quality ladder, so it accepted that undersized result. A 151 MB 4K recording asked for 40-50 MB came back as 12.5 MB of 576p.
It now recognises a saturated encoder and spends the spare budget on pixels instead of bitrate, climbing back towards the source resolution and frame rate. The step size comes from the measured bits-per-pixel of the encode that undershot, so it converges in a few attempts rather than one rung at a time.
The ceiling is still absolute - the climb stops before crossing it.
Also: derived frame rates no longer carry false precision (a 62.4999 fps source halves to 31.25 fps, not 31.2499 fps).
417 tests, 86% coverage, verified on Linux, macOS and Windows across Python 3.10-3.13.
ecompress 2.0.0
pip install ecompress
ecompress "video.mp4" 50Renamed from compress-cli. No functional changes - every behaviour in 1.3.0 is identical, only the names moved.
| Before (1.3.0) | Now (2.0.0) | |
|---|---|---|
| Install | pip install compress-cli |
pip install ecompress |
| Command | compress file.mp4 50 |
ecompress file.mp4 50 |
| Import | from compress import compress |
from ecompress import compress |
The function is still compress().
The old import name shadowed an unrelated compress package on PyPI, and the old command collided with /usr/bin/compress - the classic Unix .Z tool - on Linux and macOS. compressor was already taken, so ecompress it is.
compress-cli 1.1.0-1.3.0 stay published and keep working.
Verified on Linux, macOS and Windows across Python 3.10-3.13.
compress-cli 1.2.0
pip install --upgrade compress-cliNo functional changes. Everything under src/compress/ is byte-identical to 1.1.0, so upgrading changes nothing about how the tool behaves. This release carries repository-only work and exercises the tag-driven release path end to end.
Changed
- Pinned
ruffto the 0.16 series in thedevextra. Ruff 0.16 began formatting Python code blocks inside Markdown, which 0.15 did not, so an unpinned range let CI and a developer machine disagree aboutruff format --check. - The release workflow now fails immediately with an actionable message when a PyPI API token secret is missing, rather than falling through to Trusted Publishing and reporting an opaque OIDC error.
- Publishing a version already present on an index is skipped instead of failing, so re-running a release is idempotent.
- CI installs FFmpeg only as a package dependency so a broken bundle cannot be masked, with a separate job covering the system-
PATHfallback. - README documents the automatic FFmpeg install,
compress --check, and where each component is installed.
Verified on Linux, macOS and Windows across Python 3.10-3.13.
compress-cli 1.1.0
First public release.
pip install compress-cli
compress "video.mp4" 50Compress any file below a target size in MB. The second argument is the maximum size of the result (1 MB = 1,000,000 bytes), and it is a hard ceiling: every candidate encode is measured on disk and re-parsed by an independent reader (Pillow, ffprobe, pikepdf) before it can be reported as a success. A missed target raises an error instead of being passed off as one.
Supported: images (JPEG, PNG, WebP, AVIF, GIF, BMP, TIFF), video (MP4, MKV, MOV, WebM, AVI and more), audio (MP3, WAV, M4A, AAC, FLAC, OGG, Opus), and PDF.
FFmpeg is installed automatically on Windows x64, macOS and Linux x86_64 - no separate install, nothing on your PATH. Run compress --check to see what was found.
The original file is never modified. Output goes next to it as <name>_compressed<ext>.
Verified on Linux, macOS and Windows across Python 3.10-3.13.