# The image pipeline

Four scripts that do the four jobs an image pipeline actually has to do: make a branded frame from
data, derive the sizes everything else needs, put a set in front of a person for review, and keep an
index of what exists. Standard library plus Pillow, nothing else.

Apex Instruments is fictional. Every name, figure and contact in the input files is demo data.

## Requirements

Python 3.8 or newer. Pillow is optional. Every script checks for it and, when it is missing, writes
SVG instead of PNG and says so on the first line of its output. `index_folder.py` never needs it at
all, because it reads the PNG header and the SVG viewBox directly.

```
python3 -c "import PIL; print(PIL.__version__)"     # optional
pip install Pillow                                   # if you want the raster output
```

## Run them in this order

From this folder:

```
python3 social_frame.py                 # 6 frames, 1080x1080, into ../out/frames
python3 derive_sizes.py                 # 5 sizes per source, into ../out/derived
python3 contact_sheet.py                # one review page, into ../out/contact-sheet.*
python3 index_folder.py                 # ../out/index.json and ../out/index.md
```

Each one is independent and takes `--src` and `--out`, so they also run against your own folders.
`python3 <script>.py --help` prints every option.

## What each one does

### `social_frame.py`
Reads `input/frames.json` and writes one image per record at 1080 by 1080. Four frame kinds are
supported: `announcement`, `stat`, `quote`, `event`. Copy lives in the data file, brand values live
in `brandkit.py`, and the script only decides where things sit, which is why adding a frame is a
paragraph of JSON rather than a design job. The brand allows one orange accent per view, so a stat
frame spends it on the figure and every other kind spends it on the rule above the footer.

```
python3 social_frame.py --data input/frames.json --out ../out/frames --format both
```

Writes `frames-built.json` next to the images, listing every file with its byte count.

### `derive_sizes.py`
Takes a folder of images and derives five standard sizes from each, with the aspect handled properly
rather than by squashing:

| preset | size | fit | used for |
| --- | --- | --- | --- |
| og | 1200 x 630 | cover | Link preview on a page or a post |
| card | 640 x 360 | cover | Card image in a listing |
| thumb | 400 x 400 | cover | Square thumbnail in the asset index |
| portrait | 1080 x 1350 | cover | Tall social placement |
| email | 600 x 200 | contain | Header strip in an email, padded not cropped |

**cover** scales until the target is filled, then crops to the target from a focal point rather than
from the middle, so a wide crop of a square keeps the headline instead of the empty band under it.
Focal points live in `input/focals.json` as a fraction of width and height, and default to the
centre.

**contain** scales until the whole image fits and pads the rest with the brand page colour.

**Enlargement is a threshold, not a ban.** A small amount of upsampling is invisible and useful, so
it is allowed up to 1.2 times by default (`--max-upscale`). Past that the image is padded instead of
stretched. With the frames in this folder that produces one of each: `og` needs 1.11 times and gets
a cover crop, `portrait` would need 1.25 times and gets padded, and the record says which happened.

Every decision is written to `../out/derived/derivatives.json`: source size, target size, mode,
scale, crop box, padding, focal point, and a plain sentence for each thing that happened. A person
can check the maths without opening an image.

### `contact_sheet.py`
Lays every image in a folder onto one page with its file name and pixel size printed under it. The
raster sheet pastes resized copies of the source PNGs. The vector sheet nests each source SVG inside
the page, so it is the same artwork rather than a picture of it.

```
python3 contact_sheet.py --src ../out/derived/og --cols 3 --title "Link previews"
```

### `index_folder.py`
Walks a folder, records the path, kind, byte count, pixel size, aspect ratio and a CRC32 of the
content of every file, groups files whose content is identical, and writes `index.json` for a
machine and `index.md` for a person. It records no modification times, so the same folder always
produces the same index and any change in the file is a real change in the content.

```
python3 index_folder.py --src ../out --ext .png,.svg
```

## `brandkit.py`

Not a script, the shared part. It holds the brand colours, the font stacks, the spark mark, a text
width estimate, a line wrapper, and the two renderers.

The design that makes the whole folder work is that **a layout is computed once as a plain list of
shapes**, and that list is then handed to either the raster renderer or the vector one. The two
renderers never lay anything out themselves. That is why the SVG a web page shows and the PNG a
designer downloads are the same picture, why line breaks land in the same place in both, and why
losing Pillow degrades the output format rather than breaking the run.

Line breaking uses an average advance width per family rather than the installed font's real
metrics, deliberately, so both renderers break in the same place whatever face a given machine has.
It is an estimate, and a headline set very close to the column width can break one word earlier than
a typesetter would.

## Output sizes

The full run writes about 1.1 MB into `../out/`: 6 frames, 60 derivatives, one contact sheet in two
formats, and the index. PNGs are written as palette images, which is roughly half the bytes of
truecolour with no visible difference on flat brand artwork.
