Datagrunt 4.5.1: Sort-Friendly PDF Image Filenames

July 7, 2026by Martin Graham

Datagrunt 4.5.1 is a patch release with one user-facing improvement — every image file Datagrunt writes from a PDF now has a filename that sorts correctly — plus fixes to our release pipeline so documentation and posts like this one publish themselves.


Zero-Padded Filenames That Sort in Page Order

Datagrunt 4.5.0 introduced whole-page rasterization via PDFWriter.render_pages_as_images(). Its output files were named with plain page numbers (report_page_1.png), and plain numbers have a well-known failure mode: any tool that sorts filenames lexicographically — ls, sorted(os.listdir()), cloud storage consoles — puts them out of page order once a document passes nine pages:

# Before: lexicographic order breaks page order
report_page_1.png
report_page_10.png
report_page_11.png
report_page_2.png

In 4.5.1, every number embedded in an output filename is 1-based and zero-padded to two digits, so lexicographic order and page order are the same thing:

from datagrunt import PDFWriter

writer = PDFWriter("annual_report.pdf")

# Whole-page rasterization
writer.render_pages_as_images(output_dir="page_renders", dpi=150)
# page_renders/annual_report_page_01.png
# page_renders/annual_report_page_02.png
# ...
# page_renders/annual_report_page_10.png

# Embedded-image extraction
writer.extract_images(output_dir="extracted_images")
# extracted_images/annual_report_page01_img01.png
# extracted_images/annual_report_page02_img01.png

The convention is defined once, in a new datagrunt.core.pdf_io.filenames module, and both the PDFium and PyMuPDF engines build their filenames through it — so the two engines always produce identical names, and the rule can’t drift between code paths. Regression tests pin the behavior on both engines.

Heads-Up: Extracted-Image Names Also Changed Base

If you parse extracted-image filenames, note one deliberate semantic change. Before 4.5.1, embedded-image files used 0-based page and image indexes — report_page0_img0.png meant first page, first image — which contradicted the 1-based page field in Datagrunt’s parsed output and its element ids. Extracted-image names are now 1-based like everything else: the same file is report_page01_img01.png. If your pipeline maps those numbers back to pages, adjust for both the padding and the base shift when you upgrade.

Two digits cover documents up to 99 pages, which is where the padding sits today; wider padding is deferred until a real need shows up.


Release Pipeline: Docs and Posts Now Publish Themselves

This release also repairs the automation that produces these posts. Three fixes landed:

  • The docs workflow never self-triggered. It listened for v-prefixed tag pushes, but our version bumper pushes unprefixed tags (4.5.1) pointing at a commit whose skip-ci directive suppresses push-triggered workflows entirely. The workflow now chains off the version bump completing successfully (workflow_run) — the same mechanism our PyPI publish already uses — with a manual dispatch as fallback.
  • Release notes now cover the whole release. The generator previously diffed against HEAD~1, i.e. whatever single commit happened to precede the release. It now diffs against the previous release tag, so the post reflects everything that shipped since the last version.
  • Posts publish immediately. Generated posts previously sat as Hugo drafts awaiting manual review. They now go out with draft: false — it’s easier to push a correction to a live post than to shepherd drafts through review.

Upgrade

uv pip install --upgrade "datagrunt[pdf]"

Enjoy the release, and happy data engineering!