Skip to content

Tutorials

Three worked examples, each a complete programme that we ran against the files it names before writing it up. The API reference has every name; the policy table says what each limit bounds and why. All three turn on one idea: the check runs first, on the file's own bytes, and only a file that passes is handed to a decoder.

Opening an image with Pillow, checked first

sillybos.pillow.open_checked is two lines of code, and it is worth knowing both of them:

require(path, policy)
return Image.open(path, formats=pillow_formats(policy))

The first line is the check: size, real format, name against contents, the header, the limits. Anything outside the policy raises Refused and Pillow never hears of the file. The second line opens the file with formats set from the policy, ["JPEG", "PNG", "TIFF"] by default, so Pillow consults only those readers, whatever the file claims to be. That one argument was the whole of the fix for the October 2026 Pillow advisories; the check in front of it is what makes the limits bite before a pixel is allocated.

Install the extra, then the one-liner:

pip install 'sillybos[pillow]'
from sillybos.pillow import open_checked

with open_checked("scans/page-001.png") as image:
    print(image.format, image.size, image.mode)  # PNG (2480, 3508) L

A refusal is an exception with the verdict on it. The verdict says what the file really is, what its name claimed, and every reason it was refused:

from sillybos import Refused
from sillybos.pillow import open_checked

try:
    open_checked("inbox/page.png")
except Refused as refusal:
    print(refusal)  # page.png: refused. says PNG, is PostScript.
    verdict = refusal.verdict
    verdict.format, verdict.claimed  # ('postscript', 'png')
    verdict.reasons  # ('says PNG, is PostScript',)

Two things to know about the edges. A PDF passes the default policy, since a PDF is one of the four formats, but Pillow does not read PDFs, so open_checked on one raises Pillow's UnidentifiedImageError. If a code path only ever wants images, say so in the policy, and a PDF becomes a refusal in sillybos's words rather than an error in Pillow's:

from sillybos import Policy, Refused
from sillybos.pillow import open_checked

IMAGES = Policy(formats={"jpeg", "png", "tiff"})

try:
    open_checked("scans/book.pdf", IMAGES)
except Refused as refusal:
    print(refusal.verdict.reasons)
    # ('is PDF, which this policy does not allow (allowed: JPEG, PNG, TIFF)',)

Wrapping a call of your own

open_checked is a wrapper, and the same shape wraps anything. Here is a thumbnailer. Pillow decodes nothing when it opens a file; the pixels are read on first use, which here is the thumbnail call, and by then the check has already said the frame is within max_pixels:

from pathlib import Path

from sillybos import DEFAULT, Policy
from sillybos.pillow import open_checked


def thumbnail(path: Path, out: Path, policy: Policy = DEFAULT) -> None:
    """Make a 300-pixel thumbnail, but only of a file the policy passes."""
    with open_checked(path, policy) as image:  # the check, then Image.open(formats=...)
        image.thumbnail((300, 300))  # the first time any pixel is decoded
        image.save(out)

For a reader that has no formats argument, the check is the whole guard, and it still removes every format you did not mean to read, every file too large to be safe and the rare codings a scanner never writes. The pattern is the same two lines, with your reader in place of Image.open:

from sillybos import require


def read_with(reader, path, policy=DEFAULT):
    require(path, policy)
    return reader(path)


array = read_with(tifffile.imread, "scans/register.tif", Policy(formats={"tiff"}))

The same goes for imageio, OpenCV's imread, rawpy, or a command run with subprocess. What the check cannot do is protect against a flaw in the reader you do allow, on a file of a sensible size in a format you meant to read; the README's what it does not do says so plainly, and the security watch records, advisory by advisory, which published flaws a check in front of the decoder refuses and which it cannot.

The policy is yours

The same file, under the two presets:

from sillybos import DEFAULT, STRICT, check

for policy in (DEFAULT, STRICT):
    verdict = check("inbox/poster.jpg", policy)
    print(verdict.ok, verdict.reasons)

# True  ()
# False ('a page of 63,000,000 pixels; the limit is 50,000,000',)

DEFAULT is for a trusted laptop and your own scanner. STRICT is for files from outside. Neither is a substitute for a policy set from your own corpus: find the largest real file you have and the longest real book, multiply by two or three, and write those numbers down as a Policy in one place. A limit with a reason beside it is one a colleague can raise with confidence when the collection grows.

Rendering a PDF with poppler

A PDF is checked without being rendered. sillybos runs poppler's pdfinfo in a separate process with a time limit, and from its output knows the page count, whether the file is encrypted, and the size of every page in points. At the policy's render_dpi, a page's size in points becomes the pixels the renderer would allocate, so a page too large is refused before anything draws it:

from sillybos import Policy, check

BOOKS = Policy(formats={"pdf"}, max_pages=200, render_dpi=200)

verdict = check("scans/book.pdf", BOOKS)
print(verdict)  # book.pdf: ok (PDF, 1653x2339, 4 pages, 0.1 MB)

facts = verdict.facts
facts.pages, facts.pages_exact  # (4, True)
facts.width, facts.height  # (1653, 2339): the largest page, at 200 dpi
facts.max_pixels  # 3866596
facts.encrypted  # False

The renderer then runs at the dpi the check was made at, so what it allocates is what was checked, give or take a pixel of rounding. Knowing the page count before rendering also means the time limit can be a budget per page rather than a guess for the whole file:

import subprocess
from pathlib import Path

from sillybos import Policy, require

BOOKS = Policy(formats={"pdf"}, max_pages=200, render_dpi=200)


def render(pdf: Path, out_dir: Path, policy: Policy = BOOKS) -> list[Path]:
    """Render every page with pdftoppm, at the dpi the policy was checked at."""
    facts = require(pdf, policy).facts  # Refused if too long, too large, encrypted, or not a PDF
    out_dir.mkdir(exist_ok=True)
    subprocess.run(
        [
            "pdftoppm",
            "-r",
            str(policy.render_dpi),
            "-gray",
            "-png",
            str(pdf),
            str(out_dir / "page"),
        ],
        check=True,
        timeout=60 * facts.pages,  # a minute a page, now that the count is known
    )
    return sorted(out_dir.glob("page-*.png"))

pdf2image, pypdfium2 and the rest take the same place as pdftoppm here: the check first, then the renderer, at the same dpi. And the refusals read as they do for images. A PostScript file called page.pdf is says PDF, is PostScript; a book longer than the policy allows is 4 pages; the limit is 2, with facts.pages on the verdict so the log can say how long it was.

Taking files from outside

An upload handler, a shared inbox, a hard drive from a donor: the files were made by someone else, and the question is the same for each. The programme below decides about every file in an inbox, moves it to accepted or quarantine, and writes the verdict beside it, so that whoever looks at the quarantine later can read why without running anything.

It uses check rather than require, because here every reason matters, not just the first; and a policy of its own, set from the real collection, with a comment on each limit saying where the number came from:

import json
import shutil
from datetime import datetime, timezone
from pathlib import Path

from sillybos import Policy, check

UPLOADS = Policy(
    formats={"jpeg", "png", "tiff", "pdf"},
    max_bytes=64 * 2**20,  # 64 MiB: our largest real scan is 21 MiB
    max_pixels=40_000_000,  # our largest real page is 17 million pixels
    max_pages=120,  # our longest book is 58 pages
    png_colour_types={"greyscale", "rgb"},  # a scan is never a palette image
)

ACCEPTED = Path("accepted")
QUARANTINE = Path("quarantine")
for directory in (ACCEPTED, QUARANTINE):
    directory.mkdir(exist_ok=True)


def receive(upload: Path) -> bool:
    """Decide about a file we did not make, and keep the decision with it."""
    verdict = check(upload, UPLOADS)
    record = {
        "received": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "name": upload.name,
        "format": verdict.format,
        "ok": verdict.ok,
        "reasons": list(verdict.reasons),
        "summary": verdict.summary(),
    }
    dest = (ACCEPTED if verdict.ok else QUARANTINE) / upload.name
    shutil.move(upload, dest)
    dest.with_suffix(dest.suffix + ".json").write_text(json.dumps(record, indent=2))
    return verdict.ok


for upload in sorted(Path("inbox").iterdir()):
    print("accepted" if receive(upload) else "quarantined", upload.name)

Run over an inbox with a real scan, a GIF, a PostScript file called page.png and a 63-megapixel poster:

accepted    cover.png
quarantined invoice.gif
quarantined page.png
quarantined poster.jpg

and the records say why: is GIF, not one of the formats a scan can be (allowed: JPEG, PDF, PNG, TIFF); says PNG, is PostScript; a page of 63,000,000 pixels; the limit is 40,000,000. The accepted file's record carries its summary, PNG, 800x800, 1 page, 0.0 MB, which is worth keeping too: it is what the file looked like on the day it arrived.

Two habits make this hold up. Check a file you own: write an upload to a directory nobody else can write to, then check and open that copy, so that what the decoder opens is the same bytes the check read. And keep the policy and the decoder's own limits in agreement. If the renderer runs at 300 dpi, say render_dpi=300; if Pillow is allowed JPEG, PNG and TIFF, say so in formats, and let pillow_formats carry the same list to Image.open.

For a shell pipeline, the command does the same job: sillybos --json inbox/* prints one verdict per line, and sillybos -q --strict inbox/* prints only the refusals and exits 1 if there were any.