Metadata-Version: 2.4
Name: cardbleed
Version: 0.2.0
Summary: Extend the borders of card scans for printing — continues the existing border pattern without re-encoding the original image data
Project-URL: Repository, https://github.com/ErikBavenstrand/cardbleed
Project-URL: Issues, https://github.com/ErikBavenstrand/cardbleed/issues
Author: Erik Bävenstrand
License-Expression: MIT
License-File: LICENSE
Keywords: bleed,border,card,image,printing,proxy,tcg
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Classifier: Topic :: Printing
Requires-Python: >=3.11
Requires-Dist: jpeglib>=1.0
Requires-Dist: numpy>=1.26
Requires-Dist: pillow>=10
Requires-Dist: rich-click>=1.8
Description-Content-Type: text/markdown

# cardbleed

[![CI](https://github.com/ErikBavenstrand/cardbleed/actions/workflows/ci.yml/badge.svg)](https://github.com/ErikBavenstrand/cardbleed/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/cardbleed)](https://pypi.org/project/cardbleed/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

Extends the borders of card scans so a printed and cut card doesn't end up
with a border that is too thin. The extension continues whatever border the
card already has (holofoil speckle, solid colors, gradients), and the
original image data is never re-encoded: PNG and WebP pixels stay
bit-identical, and JPEGs are extended by splicing DCT coefficient blocks
around the untouched originals.

<table>
<tr>
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card.png" width="180"><br><sub>input, 400×550</sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_pattern.png" width="200"><br><sub>output, <code>cardbleed demo_card.png -e 24</code></sub></td>
</tr>
</table>

The demo card is generated by a script
([examples/make_demo.py](examples/make_demo.py)), so the repository contains
no copyrighted scans. It has the traits that make real scans annoying: a
speckled border, a brightness gradient across it, a scanner-bloom line at the
very edge, and an inner frame line. Bloom is trimmed and the frame line is
detected automatically, so sampling never crosses into it.

## Install

```bash
uv tool install cardbleed        # or: pipx install cardbleed
uvx cardbleed card.png           # or run once without installing
```

Until the first PyPI release, install from GitHub instead:
`uv tool install git+https://github.com/ErikBavenstrand/cardbleed`

## Usage

```bash
cardbleed card.png --compare              # extend 16px, write a comparison sheet
cardbleed ./cards/ -e 2.5mm --recursive   # batch a folder, mm-based sizing
cardbleed card.jpg -e 20 --fix-aspect     # pad to the 63x88 ratio, then extend
cardbleed card.png --target 69x94mm       # pad to an exact final size
```

Outputs are written next to the input (or to `--out-dir`) with an `_ext`
suffix. Inputs are never overwritten. `--compare` also writes a side-by-side
sheet with the original boundary marked, which makes it easy to check the
seam.

## Modes

Zoomed left-edge detail, one panel per setting: smart, pattern, naive,
mirror, soft.

<img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_detail_modes.png" width="740">

- `--mode pattern` (default) keeps structure intact: every output line is a
  real contiguous border line, and each outward pass is shifted along the
  edge by a random offset. If the border has a repeating pattern, detected by
  autocorrelation, the continuation and the offsets snap to its period so the
  pattern stays in phase. With `--shuffle 0` it degrades to a plain
  deterministic mirror.
- `--mode smart` resamples the border band stochastically. Speckle is
  re-randomized in both directions, so nothing streaks or repeats, at the
  cost of some texture structure.
- `--mode naive` replicates the outermost line straight outward (plus noise
  and smudge). Mostly useful as a baseline; it streaks on textured borders.

The gallery variants above, for reference:

<table>
<tr>
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_smart.png" width="180"><br><sub><code>--mode smart</code></sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_naive.png" width="180"><br><sub><code>--mode naive</code></sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_soft.png" width="180"><br><sub><code>--smudge 2.5 --noise 0.8</code></sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_smart_compare.png" width="180"><br><sub><code>--compare</code> sheet</sub></td>
</tr>
</table>

## Format handling

| Format | What happens to the original data |
| --- | --- |
| PNG | re-serialized losslessly; pixels bit-identical |
| WebP | written as lossless WebP; decoded pixels preserved exactly |
| JPEG | original quantized DCT blocks are copied bit-exact into a larger coefficient grid; only the new border blocks are encoded, using the file's own quantization tables |

For JPEG the extension amounts have to align to the MCU grid (8 or 16 px).
The remainder is shifted between opposite edges, so the final dimensions are
still exactly what you asked for.

## Options

`cardbleed --help` has the full reference. The ones worth knowing:

| Flag | Default | Meaning |
| --- | --- | --- |
| `-e, --extend` | `16` | Amount per edge, px or mm (`2.5mm`); per-edge overrides via `--left` etc. |
| `--fix-aspect` | off | Pad the short axis to the card ratio (`--card-size`, default `63x88` mm) before extending |
| `--target` | none | Pad to an exact final size instead, e.g. `69x94mm` |
| `--mode` | `pattern` | `pattern`, `smart`, or `naive` (see above) |
| `-k, --sample` | `12` | Band depth to sample from; clamped at detected inner border structure |
| `--trim` | `auto` | Scanner-bloom lines to cut per edge |
| `--shuffle` | `48` | How far along the edge texture may be borrowed from |
| `--noise`, `--smudge` | `0.35`, `0.6` | Added grain (relative to the border's own) and ramped blur |
| `--seed` | `0` | Output is deterministic per file |

## How it works

Each edge is analyzed on the original image: bloom lines are trimmed and the
sampling band is clamped before inner border structure. The border is split
into a smooth tone component, which is continued outward mirrored so
gradients stay seam-continuous, and a texture residual, which is resampled
according to the selected mode. Noise matched to the border's measured grain
and a ramped blur are applied on top. Corners are filled in two passes so
they inherit synthesized side texture. All randomness ramps in from zero at
the seam, so the first synthesized line is an exact continuation of the edge.

## Development

```bash
git clone https://github.com/ErikBavenstrand/cardbleed && cd cardbleed
uv run cardbleed --selfcheck            # assertion suite (fixtures)
uv run cardbleed --selfcheck scan.png   # plus checks against a real scan
uv run --group dev ruff check src
uv run --group dev pyright
```

Module layout: `synthesis.py` (edge analysis and border synthesis),
`formats.py` (format-preserving I/O, including the JPEG DCT path),
`sizing.py` (px/mm/target/aspect math), `process.py` (per-file pipeline),
`cli.py`, `selfcheck.py`.

## License

[MIT](LICENSE)
