API reference¶
Everything the package exports, generated from the docstrings. The policy table says what each limit bounds and why.
sillybos: decide before decoding.
A σίλλυβος (sillybos, Latin sittybus) was the little parchment tag that hung from a rolled scroll in an ancient library, giving its title and author so that a reader could tell what a scroll was without unrolling it. This library is that tag for the files a scanning or OCR pipeline is handed. It answers one narrow question before any image library or PDF renderer opens a file: is this one of the formats I mean to read, within the limits I expect, and if not, why?
It reads the first bytes to learn what the file really is, whatever its name
says; reads the header of a JPEG, PNG or TIFF with code of its own, never a
decoder's; asks poppler's pdfinfo and pdfdetach, in a separate process,
how many pages a PDF has, how large they are and whether it carries
JavaScript, a form or attached files; and refuses, with a reason in plain
words, anything outside a Policy.
from sillybos import check
verdict = check("scan.png")
verdict.ok, verdict.reasons # (False, ('says PNG, is PostScript',))
The whole surface is check, which returns a Verdict;
require, which raises Refused instead; Policy,
with the presets DEFAULT and STRICT; and, with the
[pillow] extra, sillybos.pillow.open_checked.
The check¶
check
¶
Decide whether a decoder may open path, without opening it with one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | PathLike[str]
|
The file to check. It must exist; a missing file is the
caller's error, not a verdict, and raises |
required |
policy
|
Policy
|
The limits to apply. sillybos.DEFAULT if not given. |
DEFAULT
|
Returns:
| Type | Description |
|---|---|
Verdict
|
A Verdict. Never raises for anything found in the file; a malformed header, a lying name and a page too large are all reasons, not exceptions. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the file does not exist. |
PdfInfoMissing
|
If the file is a PDF and |
Source code in src/sillybos/check.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | |
require
¶
As check, but raise Refused instead of returning a refusal.
Source code in src/sillybos/check.py
206 207 208 209 210 211 | |
Verdict
dataclass
¶
Verdict(path: Path, ok: bool, format: str | None, claimed: str | None, facts: Facts | None, reasons: tuple[str, ...])
The answer to "may a decoder open this file?".
Attributes:
| Name | Type | Description |
|---|---|---|
path |
Path
|
The file. |
ok |
bool
|
|
format |
str | None
|
What the file really is, from its contents: a name from
sillybos.FORMATS, another name the library knows only
to refuse ( |
claimed |
str | None
|
What its extension says it is, or |
facts |
Facts | None
|
What the header said, or |
reasons |
tuple[str, ...]
|
Why the file was refused, one sentence each. Empty when
|
summary
¶
summary() -> str
One line of facts: "JPEG, 4000x6000, 1 page, 2.1 MB".
Source code in src/sillybos/check.py
133 134 135 136 137 138 139 140 | |
Facts
dataclass
¶
Facts(bytes: int, width: int, height: int, pages: int, max_pixels: int | None, max_side: int | None, encrypted: bool = False, pages_exact: bool = True, coding: str | None = None, compression: str | None = None, metadata: tuple[str, int] | None = None, colour: str | None = None, depth: int | None = None, interlaced: bool = False, tile: tuple[int, int] | None = None, subsampling: str | None = None, features: tuple[str, ...] = ())
What was read from the file before any decoder saw it.
Attributes:
| Name | Type | Description |
|---|---|---|
bytes |
int
|
The file's size. |
width |
int
|
Pixels across the first page or frame; for a PDF, at the policy's render dpi. |
height |
int
|
Pixels down, likewise. |
pages |
int
|
Pages in a PDF, frames in a TIFF or animated PNG, 1 otherwise. For a file with more than the policy allows, the walk may have stopped early: the number is then "more than" the limit. |
max_pixels |
int | None
|
The most pixels any one page or frame would decode to,
or |
max_side |
int | None
|
The longest side, in pixels, of any one page or frame;
|
encrypted |
bool
|
Whether a PDF is encrypted. Always |
pages_exact |
bool
|
|
coding |
str | None
|
How a JPEG's frame is coded, by a name from
sillybos.JPEG_FRAMES; |
compression |
str | None
|
How a TIFF's frames are compressed, by a name from
sillybos.TIFF_COMPRESSIONS; |
metadata |
tuple[str, int] | None
|
The largest block of metadata in an image, as (what it
is, bytes): a PNG chunk, a TIFF tag's data, a JPEG colour
profile. |
colour |
str | None
|
A PNG's colour type, by a name from
sillybos.PNG_COLOUR_TYPES, or a TIFF's photometric
interpretation, from sillybos.TIFF_PHOTOMETRICS;
|
depth |
int | None
|
A PNG's or TIFF's bits per sample; |
interlaced |
bool
|
Whether a PNG is Adam7 interlaced. |
tile |
tuple[int, int] | None
|
A tiled TIFF's largest tile as (width, height); |
subsampling |
str | None
|
A YCbCr TIFF's chroma subsampling, horizontal by
vertical, as |
features |
tuple[str, ...]
|
Which of sillybos.PDF_FEATURES a PDF carries: JavaScript, a form, attached files, a launch action. Empty for an image and for a PDF with none. |
PdfInfoMissing
¶
PdfInfoMissing(tool: str = 'pdfinfo')
Bases: RuntimeError
A poppler tool (pdfinfo, pdfdetach) is not on the PATH, so no PDF can be checked.
Source code in src/sillybos/pdf.py
69 70 71 72 73 | |
The policy¶
Policy
dataclass
¶
Policy(formats: frozenset[str] = (lambda: frozenset(FORMATS))(), max_bytes: int = 512 * MIB, max_pixels: int = 89478485, max_side: int = 30000, max_pages: int = 500, max_metadata_bytes: int = 4 * MIB, render_dpi: int = 200, name_must_match: bool = True, jpeg_frames: frozenset[str] = (lambda: frozenset({'baseline', 'extended', 'progressive'}))(), png_colour_types: frozenset[str] = (lambda: frozenset(PNG_COLOUR_TYPES))(), max_png_bit_depth: int = 16, allow_interlaced_png: bool = False, allow_tiled_tiff: bool = True, tiff_photometrics: frozenset[str] = (lambda: frozenset({'greyscale', 'rgb', 'palette', 'cmyk', 'ycbcr'}))(), tiff_subsamplings: frozenset[str] = (lambda: frozenset({'1x1', '2x1', '1x2', '2x2'}))(), tiff_compressions: frozenset[str] = (lambda: frozenset({'none', 'ccitt', 'lzw', 'jpeg', 'deflate', 'packbits'}))(), allow_encrypted_pdf: bool = False, pdf_features: frozenset[str] = frozenset(), pdfinfo_timeout: float = 30.0)
The limits a file must sit inside to pass sillybos.check.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
formats
|
frozenset[str]
|
Which of FORMATS may pass. A file of any other format is refused whatever its name says. |
(lambda: frozenset(FORMATS))()
|
max_bytes
|
int
|
The largest file, in bytes. Checked before anything is
read, so an enormous file is never handed to |
512 * MIB
|
max_pixels
|
int
|
The most pixels in any one page or frame once decoded.
For a PDF page, the pixels it would render to at |
89478485
|
max_side
|
int
|
The most pixels along either side of any one page, frame or tile. A pixel budget alone lets through a page a few pixels wide and a million tall, and an abnormally tall image is what drove libtiff's RGBA reader to write colour data to an arbitrary address (CVE-2025-9900). No scanner writes a side longer than a few tens of thousands of pixels: the default is three times the longest side in the corpus (9,933 pixels, A3 at 600 dpi). |
30000
|
max_pages
|
int
|
The most pages in a PDF, or frames in a multi-frame TIFF or animated PNG. Bounds the time and the memory a run takes, and stops the header walk itself on a file with a million empty frames. |
500
|
max_metadata_bytes
|
int
|
The largest block of metadata in an image: a PNG chunk other than image data, a TIFF tag's out-of-line data, a JPEG colour profile across its segments. A scan carries a few kilobytes of EXIF and a colour profile well under a megabyte; a block the size of the file is where parsers' quadratic paths and allocation flaws have been found (CVE-2026-96643, CVE-2023-6277). Pillow refuses a PNG text chunk over 1 MiB on its own; the default leaves room for a large profile or XMP. |
4 * MIB
|
render_dpi
|
int
|
The resolution the PDF renderer will be run at, so a page's size in points can be turned into pixels before the renderer allocates them. |
200
|
name_must_match
|
bool
|
Refuse a file whose extension says one allowed
format and whose contents are another (a JPEG called
|
True
|
jpeg_frames
|
frozenset[str]
|
Which of JPEG_FRAMES may pass. The default is what a scanner writes: baseline, extended and progressive. A lossless, hierarchical or arithmetic-coded frame goes down a path of the decoder that almost no file exercises; a lossless frame was the way into a heap overflow in Pillow's JPEG decoder (CVE-2026-112403). A multi-picture JPEG's pictures count as pages and are each read. |
(lambda: frozenset({'baseline', 'extended', 'progressive'}))()
|
png_colour_types
|
frozenset[str]
|
Which of PNG_COLOUR_TYPES may pass. The default is all five; a policy for scans alone can keep to greyscale and RGB, which closes libpng's palette paths (CVE-2025-64720, CVE-2025-66293, CVE-2026-33636). |
(lambda: frozenset(PNG_COLOUR_TYPES))()
|
max_png_bit_depth
|
int
|
The deepest PNG sample. A scanner writes 8 bits; some archival software writes 16-bit greyscale, so the default allows it and the strict preset does not. |
16
|
allow_interlaced_png
|
bool
|
Pass an Adam7-interlaced PNG. No scanner writes one, and the interlaced 16-bit path was the way into two libpng overflows (CVE-2025-65018, CVE-2026-22695), so the default refuses. |
False
|
tiff_photometrics
|
frozenset[str]
|
Which of TIFF_PHOTOMETRICS may pass. The default is what a scanner writes: greyscale, RGB, palette, CMYK and YCbCr. Every frame of a file must share one. |
(lambda: frozenset({'greyscale', 'rgb', 'palette', 'cmyk', 'ycbcr'}))()
|
tiff_subsamplings
|
frozenset[str]
|
Which of TIFF_SUBSAMPLINGS a YCbCr TIFF may declare. The default is the four a scanner writes; the factor-of-four paths are where libtiff's most recent heap write was found (CVE-2026-4775). |
(lambda: frozenset({'1x1', '2x1', '1x2', '2x2'}))()
|
allow_tiled_tiff
|
bool
|
Pass a TIFF laid out in tiles rather than strips.
A scanner writes strips; digitisation and mapping software
write tiles, so the default allows them and the strict preset
does not. A tile is what the decoder allocates whole, so a
tile is also held to |
True
|
tiff_compressions
|
frozenset[str]
|
Which of TIFF_COMPRESSIONS may pass. The default is what a scanner writes: none, CCITT, LZW, JPEG, Deflate and PackBits. The other codecs are where most of libtiff's flaws have been found (PixarLog most recently, CVE-2026-12912), and a scan never needs them. Every frame of a file must use the same scheme. |
(lambda: frozenset({'none', 'ccitt', 'lzw', 'jpeg', 'deflate', 'packbits'}))()
|
allow_encrypted_pdf
|
bool
|
Pass a PDF that |
False
|
pdf_features
|
frozenset[str]
|
Which of PDF_FEATURES a PDF may carry:
JavaScript, a form, attached files, a launch action. Each is a
way for a document to make the viewer act rather than draw, and
a scanned book has none, so the default allows none. Forms are
read from |
frozenset()
|
pdfinfo_timeout
|
float
|
Seconds to give |
30.0
|
STRICT
module-attribute
¶
STRICT = Policy(max_bytes=128 * MIB, max_pixels=50000000, max_side=15000, max_pages=100, max_metadata_bytes=1 * MIB, max_png_bit_depth=8, allow_tiled_tiff=False)
For files from outside: smaller, shorter, and no more lenient on format.
The names¶
What a policy's fields and a verdict's facts are spelt with. Each tuple is every name the library knows, in the order the reasons list them; a policy chooses from it.
FORMATS
module-attribute
¶
FORMATS: tuple[str, ...] = ('jpeg', 'png', 'tiff', 'pdf')
The only formats the library will ever pass: the ones a scan can be. Everything else is refused by name. Adding a format is adding attack surface, so one is added only when someone shows a scan in it.
JPEG_FRAMES
module-attribute
¶
JPEG_FRAMES: tuple[str, ...] = ('baseline', 'extended', 'progressive', 'lossless', 'hierarchical', 'arithmetic')
Every way a JPEG frame can be coded, as its SOF marker says. A scanner writes the first three. The rest are legal, rare, and the decoder's least exercised code.
PDF_FEATURES
module-attribute
¶
PDF_FEATURES: tuple[str, ...] = ('javascript', 'forms', 'attachments', 'launch')
Everything a PDF can carry that makes a viewer do something other than draw the page: JavaScript, an interactive form, attached (embedded) files, and a launch action that runs a program. A scanned book has none of them, so the default policy refuses them all; a policy names the ones it will pass.
PNG_COLOUR_TYPES
module-attribute
¶
PNG_COLOUR_TYPES: tuple[str, ...] = ('greyscale', 'rgb', 'palette', 'greyscale-alpha', 'rgba')
Every colour type a PNG can have, by name. A scanner writes greyscale or RGB; the palette paths are where several of libpng's recent flaws were.
TIFF_COMPRESSIONS
module-attribute
¶
TIFF_COMPRESSIONS: tuple[str, ...] = ('none', 'ccitt', 'lzw', 'jpeg', 'deflate', 'packbits', 'old-jpeg', 'next', 'thunderscan', 'it8', 'pixar', 'jbig', 'sgilog', 'jpeg2000', 'lerc', 'lzma', 'zstd', 'webp', 'jxl')
Every TIFF compression scheme the library knows, by name. A scanner writes the first six. The rest are codecs in libtiff that a scan never needs and that most of its security history lives in; a code outside this list is refused whatever the policy says.
TIFF_PHOTOMETRICS
module-attribute
¶
TIFF_PHOTOMETRICS: tuple[str, ...] = ('greyscale', 'rgb', 'palette', 'cmyk', 'ycbcr', 'mask', 'cielab', 'icclab', 'itulab', 'cfa', 'logl', 'logluv', 'linear-raw')
Every photometric interpretation a TIFF can declare, by name. A scanner writes the first five (YCbCr is what a JPEG-compressed scan is); the rest are colour models a scan never uses. A value outside this list is refused whatever the policy says.
TIFF_SUBSAMPLINGS
module-attribute
¶
TIFF_SUBSAMPLINGS: tuple[str, ...] = ('1x1', '2x1', '1x2', '2x2', '4x1', '1x4', '4x2', '2x4', '4x4')
Every chroma subsampling a YCbCr TIFF can declare, horizontal by vertical. A scanner writes the first four; the factor-of-four paths are the least exercised in libtiff (CVE-2026-4775).
With Pillow¶
Check a file, then open it with Pillow using only the readers the policy allows.
The one-line fix that October 2026's Pillow advisories made everyone want,
packaged: Image.open takes a formats list, and a file that is not one
of them is never handed to the other readers, whatever it claims to be.
open_checked runs sillybos.require first, so a file that is
too large, lies about its format or has a page beyond the limit is refused
before Pillow sees it, and then opens it with formats set from the
policy.
Needs the [pillow] extra: pip install 'sillybos[pillow]'.
pillow_formats
¶
The formats argument for Image.open, from a policy: ["JPEG", "PNG", "TIFF"].
Source code in src/sillybos/pillow.py
28 29 30 | |
open_checked
¶
Check path against policy, then open it with only the allowed Pillow readers.
A PDF passes the check but Pillow does not read PDFs; opening one raises
PIL.UnidentifiedImageError, as it would for any file outside
formats.
Raises:
| Type | Description |
|---|---|
Refused
|
If the file does not pass the policy. |
ImportError
|
If Pillow is not installed. |
Source code in src/sillybos/pillow.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |