spacr.qt.imagery¶
Photographic backgrounds for image-backed themes.
Why this exists next to spacr.qt.space¶
spacr.qt.space generates procedural backgrounds. This module prepares
three bundled photographs—two microscopy images and one deep-field astronomy
image—for the Space and Cell themes.
It reuses space’s cache directory, its size clamps and its
“never raise, fall back to something” contract, so from the outside a
photo background and a generated one behave identically. What is new is
the part that photographs need and procedural pixels do not: a decode
budget, a crop policy, and a measured legibility check.
Decoded-image memory, not compressed file size, sets the performance limit. Large source photographs are therefore prepared as follows:
the masters shipped in
spacr/resources/themesare already cropped and capped atMASTER_CAP(3840x2400 —spacr.qt.space.MAX_DIM), so the largest thing ever decoded at runtime is ~27 MB, not 281 MB;each screen size is rendered once and cached as JPEG under
~/.spacr/backgrounds; anddecode_count()reports master decodes for diagnostics and tests.
The crop policy¶
MASTERS records, per image, the sub-rectangle of the original
that is usable and the vertical focus for the aspect crop. Two of the
three are trimmed for a reason:
cell_2.pngcarries a burned-in “5 um” scale bar and label in the bottom right (measured at x 0.825-0.949, y 0.925-0.953 of the frame). A wallpaper is not a figure, and a stray scale bar reads as an artefact, so itssource_cropcuts above it. Because the shipped master is built from that crop, no runtime crop can bring the bar back.space_1.jpegandcell.pngcarry no burned-in annotation (checked: zero 32x32 blocks more than 35 % saturated achromatic white), so they are used whole.
Legibility is solved, not eyeballed¶
These are not a procedural sky of point stars — cell.png’s cyan core
fills most of the frame and measures 0.236 in the brightest
text-line-sized window, which is far too bright to put white-on-nothing
text over. So each master is dimmed at build time by a factor solved
from the palette: spacr.qt.theme.max_background_luma() returns the
brightest a bare window background may be before some role that gets
painted straight onto it (fg, fg_muted, accent, fg_dim,
the status hues) drops below its WCAG minimum, and
solve_dim() scales the image in linear light until the brightest
TEXT_WINDOW-sized region lands on that number less
SAFETY_MARGIN.
The same solve runs again in render(), where it is a no-op on the
shipped masters and the whole guarantee for an image the user dropped
into ~/.spacr/themes themselves.
spacr.qt.space now uses the same three functions —
exposure_target(), brightest_window() and solve_dim()
— on its generated sky, which for a long time was the one wallpaper in
the app that had never been measured against the rule the photographs
were held to. It cannot call solve_dim() on the finished
frame (that lands the sky on a solid black rectangle; the numbers are in
that module’s docstring), so it applies the ceiling where the frame
actually breaks the rule and then measures the result here.
Scrimmed surfaces need no such treatment: spacr.qt.theme.contrast_report()
already judges every panel against a pure white background, which is
the worst case any photograph can present.
Functions¶
|
Keys whose master is actually present on this machine. |
|
On-disk path of |
|
Brightest |
|
Turn one original into the master that ships in the wheel. |
|
Build every master in |
|
Return the versioned cache filename for a photographic background. |
|
Delete every cached photo background. Returns the number removed. |
|
Largest sub-rectangle of the source that has the output's aspect. |
|
How many times a master has been decoded this process. |
|
Return |
|
Luminance the brightest text-line-sized region is aimed at. |
|
Measure how readable |
|
Measure how readable a wallpaper's pixels actually are. |
|
Map a uint8 (h, w, 3) image to linear-light floats in [0, 1]. |
|
WCAG relative luminance of every pixel of a uint8 RGB array. |
|
The shipped master as a uint8 (h, w, 3) array, or |
|
Directories searched for a master, most specific first. |
|
Absolute path of |
|
True when two |
|
Render |
|
Zero the decode counter. |
|
Count blocks that look like burned-in annotation. |
|
Exposure factor taking |
|
Exposure-solve an image file in place. |
|
Encode one linear-light value as an sRGB signal value in [0, 1]. |
|
Which theme's palette |
|
Human-readable name of a wallpaper, for Preferences. |
|
Directory where a user may drop replacement masters. |
Module Contents¶
- spacr.qt.imagery.available_keys() Tuple[str, ...][source]¶
Keys whose master is actually present on this machine.
- spacr.qt.imagery.background_path(key: str, width: int = 0, height: int = 0, regenerate: bool = False) pathlib.Path | None[source]¶
On-disk path of
key’s wallpaper at this size, rendering if needed.Returns
None— never raises — when it cannot be produced: no master, a read-only home directory, no image encoder. Callers treat that as “use the procedural sky” or “use the flat gradient”, so a failure here costs some prettiness and nothing else.- Parameters:
key – wallpaper key in
MASTERS, e.g."microtubules"; also names the cached file.
- spacr.qt.imagery.brightest_window(arr: numpy.ndarray, window: Tuple[float, float] = TEXT_WINDOW) Tuple[float, str][source]¶
Brightest
window-sized region of a uint8 RGB image.- Parameters:
arr – uint8 RGB image array of shape
(h, w, 3).- Returns:
(luminance, "#rrggbb")— the region’s mean relative luminance and its mean colour, which is whatspacr.qt.theme.image_contrast_report()should be handed as the thing text has to be readable over.
Averaging is done in linear light, so the returned colour’s own luminance is exactly the returned number.
- spacr.qt.imagery.build_master(key: str, src_dir, dst_dir) pathlib.Path | None[source]¶
Turn one original into the master that ships in the wheel.
Crop away burned-in annotation, take the
MASTER_ASPECTcrop around the subject, cap atMASTER_CAP, dim until the brightest text-line-sized region satisfies the theme’s palette, and write JPEG.Kept in the shipped module rather than a build script so the assets can be re-derived from the originals by anyone who has them, and so the crop rectangles live next to the code that documents why they are where they are.
- Parameters:
key – wallpaper key in
MASTERS; an unknown key raisesKeyError.src_dir – directory holding the original image named by the entry’s
"source".dst_dir – directory the JPEG master is written into, under the entry’s
"file"name; created if missing.
- Returns:
the written path, or
Noneif the original is missing.
- spacr.qt.imagery.build_masters(src_dir, dst_dir=None) Dict[str, pathlib.Path | None][source]¶
Build every master in
MASTERS. Seebuild_master().- Parameters:
src_dir – directory holding the originals, passed to
build_master()for every key.
- spacr.qt.imagery.cache_name(key: str, width: int, height: int) str[source]¶
Return the versioned cache filename for a photographic background.
- Parameters:
key – wallpaper key, embedded in the name as
photo-<key>-....width – image width in pixels.
height – image height in pixels.
- spacr.qt.imagery.clear_cache() int[source]¶
Delete every cached photo background. Returns the number removed.
- spacr.qt.imagery.cover_box(src_w: int, src_h: int, out_w: int, out_h: int, focus: float = 0.5) Tuple[int, int, int, int][source]¶
Largest sub-rectangle of the source that has the output’s aspect.
“Cover”, never “contain”: the result always fills the target, so the stylesheet — which centres the image without repeating it — can never end up letterboxing it into bands of flat colour.
- Parameters:
src_w – width of the source image in pixels.
src_h – height of the source image in pixels.
out_w – width of the target area in pixels.
out_h – height of the target area in pixels.
focus – vertical centre of the crop as a fraction of the source height. Clamped so the box stays inside the frame.
- spacr.qt.imagery.decode_count() int[source]¶
How many times a master has been decoded this process.
Every master decode goes through
_open_master(), which bumps this. A resize or a repaint must not move it: the window paints a cached, screen-sized JPEG that Qt loaded once when the stylesheet was applied.reset_decode_count()exists so a test can measure a span rather than an absolute.
- spacr.qt.imagery.dim(arr: numpy.ndarray, factor: float) numpy.ndarray[source]¶
Return
arrdarkened byfactorin linear light.- Parameters:
arr – uint8 image array; every value is mapped through a 256-entry lookup table, so any shape works.
factor – multiplier applied to linear-light values;
1.0or more returnsarrunchanged.
- spacr.qt.imagery.exposure_target(theme: str) float[source]¶
Luminance the brightest text-line-sized region is aimed at.
The palette’s hard WCAG limit, backed off by
SAFETY_MARGIN.- Parameters:
theme – theme name whose palette sets the limit, e.g.
"cell"or"space"; seespacr.qt.theme.max_background_luma().
- spacr.qt.imagery.legibility(key: str) dict | None[source]¶
Measure how readable
key’s master actually is.See
legibility_of()for the returned dict.Nonewhen the master is not installed.- Parameters:
key – wallpaper key in
MASTERS; the master is judged against that entry’s theme.
- spacr.qt.imagery.legibility_of(arr: numpy.ndarray, theme: str, key: str = '') dict[source]¶
Measure how readable a wallpaper’s pixels actually are.
Everything in the returned dict comes from the real image data:
brightestluminance of the worstTEXT_WINDOW-sized regioncolorthat region’s mean colour, as#rrggbblimitthe most that region is allowed to be, from the palettetargetwhat the exposure aimed at —limitless the marginpasseswhether it is within the limitfailuresevery WCAG rule the theme fails over that regionTakes an array rather than a registry key so the generated sky can be held to the identical measurement —
spacr.qt.space.legibility()is this function over a rendered frame. Until it was, the sky was the one background in the app that had never been measured, and it was 8-14x over the limit.- Parameters:
arr – uint8 RGB image array of shape
(h, w, 3).theme – theme name whose palette sets
limitandfailures.
- spacr.qt.imagery.linear_rgb(arr: numpy.ndarray) numpy.ndarray[source]¶
Map a uint8 (h, w, 3) image to linear-light floats in [0, 1].
- Parameters:
arr – uint8 image array; each value is looked up in a 256-entry table, so the result has the same shape.
- spacr.qt.imagery.luminance_map(arr: numpy.ndarray) numpy.ndarray[source]¶
WCAG relative luminance of every pixel of a uint8 RGB array.
- Parameters:
arr – uint8 RGB image array of shape
(h, w, 3); the result has shape(h, w).
- spacr.qt.imagery.master_array(key: str) numpy.ndarray | None[source]¶
The shipped master as a uint8 (h, w, 3) array, or
None.Decoded at 1/8 scale where the format allows it and box-averaged the rest of the way — see
_probe().- Parameters:
key – wallpaper key in
MASTERS; seemaster_path().
- spacr.qt.imagery.master_dirs() Tuple[pathlib.Path, ...][source]¶
Directories searched for a master, most specific first.
- spacr.qt.imagery.master_path(key: str) pathlib.Path | None[source]¶
Absolute path of
key’s master image, orNone.Noneis a normal outcome, not an error: a source build with the assets stripped, or an unknown key. Callers fall back to the procedural sky or to the flat gradient.- Parameters:
key – wallpaper key in
MASTERS; its"file"is looked for in each ofmaster_dirs()in turn.
- spacr.qt.imagery.rects_overlap(a: Tuple[float, float, float, float], b: Tuple[float, float, float, float]) bool[source]¶
True when two
(x0, y0, x1, y1)rectangles share any area.- Parameters:
a – first rectangle as
(x0, y0, x1, y1).b – second rectangle, same form. Rectangles that only touch along an edge do not overlap.
- spacr.qt.imagery.render(key: str, width: int, height: int)[source]¶
Render
key’s wallpaper at exactlywidthxheight.- Parameters:
key – wallpaper key in
MASTERS.width – output width in pixels, clamped to
spacr.qt.space.MIN_DIMandspacr.qt.space.MAX_DIM.height – output height in pixels, clamped the same way.
- Returns:
a PIL image, or
Nonewhen the master is missing.
The crop and the resample happen in a single
Image.resizecall with abox, so no intermediate full-resolution crop is materialised.The dim solve runs here as well as in
build_master(), and it is deliberately not redundant: on the shipped masters it resolves to a no-op because they are already at the limit, but a user who drops their own photograph into~/.spacr/themesgets the same guarantee without having to know it exists. Solving costs one pass over a 480 px thumbnail plus one 256-entry lookup table.
- spacr.qt.imagery.solid_annotation_blocks(arr: numpy.ndarray) int[source]¶
Count blocks that look like burned-in annotation.
Annotation — a scale bar, a label, a timestamp — is solid, achromatic and at the frame’s peak brightness, and it covers whole blocks. Real content in these images does not: the brightest galaxy core in the deep field fills 15 % of a block, a drawn scale bar fills 63 %.
The thresholds are relative to the image’s own 99.99th percentile rather than absolute, because these masters are deliberately exposed differently — the microtubule frame peaks at 218, not 255, and an absolute “≥ 190 is white” test would go blind on it.
- Parameters:
arr – uint8 RGB image array of shape
(h, w, 3); an empty array or one smaller thanANNOTATION_BLOCKon either side gives 0.- Returns:
number of blocks that look like annotation. Zero for every shipped master; the tests also check it is non-zero on the same masters with a bar drawn on, so a detector that has quietly stopped detecting cannot pass.
- spacr.qt.imagery.solve_dim(measured: float, target: float) float[source]¶
Exposure factor taking
measuredluminance down totarget.Closed form rather than the bisection
spacr.qt.spaceneeds, because scaling linear light scales relative luminance by exactly the same factor — there is no tone curve in the way. Never brightens: an image already dark enough is left alone.- Parameters:
measured – current linear relative luminance;
0or less gives1.0.target – luminance to reach; negative values are treated as
0.
- spacr.qt.imagery.solve_image_file(path, theme: str, fmt: str = 'JPEG') bool[source]¶
Exposure-solve an image file in place.
Truewhen it is legible.For pixels that arrive at runtime rather than in the wheel — today that is the optional NASA/ESA download in
spacr.qt.space.download_nasa_background().render()cannot help there: that file is handed to the stylesheet directly, at whatever size it arrived, so the solve has to happen to the file.False— never an exception — when it cannot be read, solved or rewritten. A caller must then refuse the image rather than install it. A wallpaper that is not bounded, under a theme whose scrims are solved against the bound, is precisely the failurespacr.qt.theme.EXPOSURE_BOUNDED_THEMESwarns about: panels thinned to what a dark sky can carry, with a solar flare behind them.- Parameters:
path – image file to read and, if it needs dimming, overwrite.
theme – theme name whose palette sets the exposure target.
- spacr.qt.imagery.srgb_encode(linear: float) float[source]¶
Encode one linear-light value as an sRGB signal value in [0, 1].
Public because
spacr.qt.spaceneeds it, and because the distinction it carries is the one that is easiest to get wrong here: every limit in this module —exposure_target(),spacr.qt.theme.max_background_luma()— is a linear relative luminance, while the sky generator’s tone map emits an sRGB signal value. Space’s 0.0586 limit is#444444, not a 6 % signal; the two readings are a factor of 4.6 apart, which is the difference between a dimmed sun and a black rectangle.- Parameters:
linear – linear-light value; clipped to
[0, 1]before encoding.
- spacr.qt.imagery.theme_for(key: str) str | None[source]¶
Which theme’s palette
keyis judged against, orNone.- Parameters:
key – wallpaper key in
MASTERS; unknown keys giveNone.
- spacr.qt.imagery.title_for(key: str) str[source]¶
Human-readable name of a wallpaper, for Preferences.
- Parameters:
key – wallpaper key in
MASTERS; an unknown key is returned as its own title.
- spacr.qt.imagery.user_dir() pathlib.Path[source]¶
Directory where a user may drop replacement masters.