cardbleed 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cardbleed-0.1.0/.github/workflows/ci.yml +29 -0
- cardbleed-0.1.0/.github/workflows/release.yml +30 -0
- cardbleed-0.1.0/.gitignore +8 -0
- cardbleed-0.1.0/LICENSE +21 -0
- cardbleed-0.1.0/PKG-INFO +164 -0
- cardbleed-0.1.0/README.md +138 -0
- cardbleed-0.1.0/examples/demo_card.png +0 -0
- cardbleed-0.1.0/examples/demo_card_mirror.png +0 -0
- cardbleed-0.1.0/examples/demo_card_naive.png +0 -0
- cardbleed-0.1.0/examples/demo_card_pattern.png +0 -0
- cardbleed-0.1.0/examples/demo_card_smart.png +0 -0
- cardbleed-0.1.0/examples/demo_card_smart_compare.png +0 -0
- cardbleed-0.1.0/examples/demo_card_soft.png +0 -0
- cardbleed-0.1.0/examples/demo_detail_modes.png +0 -0
- cardbleed-0.1.0/examples/make_demo.py +54 -0
- cardbleed-0.1.0/pyproject.toml +73 -0
- cardbleed-0.1.0/src/cardbleed/__init__.py +25 -0
- cardbleed-0.1.0/src/cardbleed/__main__.py +4 -0
- cardbleed-0.1.0/src/cardbleed/_version.py +1 -0
- cardbleed-0.1.0/src/cardbleed/cli.py +336 -0
- cardbleed-0.1.0/src/cardbleed/errors.py +2 -0
- cardbleed-0.1.0/src/cardbleed/filters.py +68 -0
- cardbleed-0.1.0/src/cardbleed/formats.py +257 -0
- cardbleed-0.1.0/src/cardbleed/process.py +177 -0
- cardbleed-0.1.0/src/cardbleed/selfcheck.py +360 -0
- cardbleed-0.1.0/src/cardbleed/sizing.py +132 -0
- cardbleed-0.1.0/src/cardbleed/synthesis.py +387 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v5
|
|
14
|
+
- run: uv run --group dev ruff check src examples
|
|
15
|
+
- run: uv run --group dev ruff format --check src examples
|
|
16
|
+
- run: uv run --group dev pyright
|
|
17
|
+
|
|
18
|
+
selfcheck:
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
strategy:
|
|
21
|
+
matrix:
|
|
22
|
+
os: [ubuntu-latest, macos-latest]
|
|
23
|
+
python: ["3.11", "3.13"]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: astral-sh/setup-uv@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python }}
|
|
29
|
+
- run: uv run cardbleed --selfcheck
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v5
|
|
13
|
+
- run: uv build
|
|
14
|
+
- uses: actions/upload-artifact@v4
|
|
15
|
+
with:
|
|
16
|
+
name: dist
|
|
17
|
+
path: dist/
|
|
18
|
+
|
|
19
|
+
publish:
|
|
20
|
+
needs: build
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: pypi
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write # PyPI trusted publishing
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/download-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
cardbleed-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erik Bävenstrand
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cardbleed-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cardbleed
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extend the borders of card scans for printing — continues the existing border pattern without re-encoding the original image data
|
|
5
|
+
Project-URL: Repository, https://github.com/ErikBavenstrand/cardbleed
|
|
6
|
+
Project-URL: Issues, https://github.com/ErikBavenstrand/cardbleed/issues
|
|
7
|
+
Author: Erik Bävenstrand
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: bleed,border,card,image,printing,proxy,tcg
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
19
|
+
Classifier: Topic :: Printing
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: jpeglib>=1.0
|
|
22
|
+
Requires-Dist: numpy>=1.26
|
|
23
|
+
Requires-Dist: pillow>=10
|
|
24
|
+
Requires-Dist: rich-click>=1.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# cardbleed
|
|
28
|
+
|
|
29
|
+
[](https://github.com/ErikBavenstrand/cardbleed/actions/workflows/ci.yml)
|
|
30
|
+
[](https://pypi.org/project/cardbleed/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
|
|
33
|
+
Extends the borders of card scans so a printed and cut card doesn't end up
|
|
34
|
+
with a border that is too thin. The extension continues whatever border the
|
|
35
|
+
card already has (holofoil speckle, solid colors, gradients), and the
|
|
36
|
+
original image data is never re-encoded: PNG and WebP pixels stay
|
|
37
|
+
bit-identical, and JPEGs are extended by splicing DCT coefficient blocks
|
|
38
|
+
around the untouched originals.
|
|
39
|
+
|
|
40
|
+
<table>
|
|
41
|
+
<tr>
|
|
42
|
+
<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>
|
|
43
|
+
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_smart.png" width="200"><br><sub>output, <code>cardbleed demo_card.png -e 24</code></sub></td>
|
|
44
|
+
</tr>
|
|
45
|
+
</table>
|
|
46
|
+
|
|
47
|
+
The demo card is generated by a script
|
|
48
|
+
([examples/make_demo.py](examples/make_demo.py)), so the repository contains
|
|
49
|
+
no copyrighted scans. It has the traits that make real scans annoying: a
|
|
50
|
+
speckled border, a brightness gradient across it, a scanner-bloom line at the
|
|
51
|
+
very edge, and an inner frame line. Bloom is trimmed and the frame line is
|
|
52
|
+
detected automatically, so sampling never crosses into it.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv tool install cardbleed # or: pipx install cardbleed
|
|
58
|
+
uvx cardbleed card.png # or run once without installing
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Until the first PyPI release, install from GitHub instead:
|
|
62
|
+
`uv tool install git+https://github.com/ErikBavenstrand/cardbleed`
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cardbleed card.png --compare # extend 16px, write a comparison sheet
|
|
68
|
+
cardbleed ./cards/ -e 2.5mm --recursive # batch a folder, mm-based sizing
|
|
69
|
+
cardbleed card.jpg -e 20 --fix-aspect # pad to the 63x88 ratio, then extend
|
|
70
|
+
cardbleed card.png --target 69x94mm # pad to an exact final size
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Outputs are written next to the input (or to `--out-dir`) with an `_ext`
|
|
74
|
+
suffix. Inputs are never overwritten. `--compare` also writes a side-by-side
|
|
75
|
+
sheet with the original boundary marked, which makes it easy to check the
|
|
76
|
+
seam.
|
|
77
|
+
|
|
78
|
+
## Modes
|
|
79
|
+
|
|
80
|
+
Zoomed left-edge detail, one panel per setting: smart, pattern, naive,
|
|
81
|
+
mirror, soft.
|
|
82
|
+
|
|
83
|
+
<img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_detail_modes.png" width="740">
|
|
84
|
+
|
|
85
|
+
- `--mode smart` (default) resamples the border band stochastically. Speckle
|
|
86
|
+
is re-randomized in both directions, so nothing streaks or repeats, at the
|
|
87
|
+
cost of some texture structure.
|
|
88
|
+
- `--mode pattern` keeps structure intact: every output line is a real
|
|
89
|
+
contiguous border line, and each outward pass is shifted along the edge by
|
|
90
|
+
a random offset. If the border has a repeating pattern, detected by
|
|
91
|
+
autocorrelation, the continuation and the offsets snap to its period so the
|
|
92
|
+
pattern stays in phase. This is usually the best choice for holo borders.
|
|
93
|
+
With `--shuffle 0` it degrades to a plain deterministic mirror.
|
|
94
|
+
- `--mode naive` replicates the outermost line straight outward (plus noise
|
|
95
|
+
and smudge). Mostly useful as a baseline; it streaks on textured borders.
|
|
96
|
+
|
|
97
|
+
The gallery variants above, for reference:
|
|
98
|
+
|
|
99
|
+
<table>
|
|
100
|
+
<tr>
|
|
101
|
+
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_pattern.png" width="180"><br><sub><code>--mode pattern</code></sub></td>
|
|
102
|
+
<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>
|
|
103
|
+
<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>
|
|
104
|
+
<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>
|
|
105
|
+
</tr>
|
|
106
|
+
</table>
|
|
107
|
+
|
|
108
|
+
## Format handling
|
|
109
|
+
|
|
110
|
+
| Format | What happens to the original data |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| PNG | re-serialized losslessly; pixels bit-identical |
|
|
113
|
+
| WebP | written as lossless WebP; decoded pixels preserved exactly |
|
|
114
|
+
| 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 |
|
|
115
|
+
|
|
116
|
+
For JPEG the extension amounts have to align to the MCU grid (8 or 16 px).
|
|
117
|
+
The remainder is shifted between opposite edges, so the final dimensions are
|
|
118
|
+
still exactly what you asked for.
|
|
119
|
+
|
|
120
|
+
## Options
|
|
121
|
+
|
|
122
|
+
`cardbleed --help` has the full reference. The ones worth knowing:
|
|
123
|
+
|
|
124
|
+
| Flag | Default | Meaning |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `-e, --extend` | `16` | Amount per edge, px or mm (`2.5mm`); per-edge overrides via `--left` etc. |
|
|
127
|
+
| `--fix-aspect` | off | Pad the short axis to the card ratio (`--card-size`, default `63x88` mm) before extending |
|
|
128
|
+
| `--target` | none | Pad to an exact final size instead, e.g. `69x94mm` |
|
|
129
|
+
| `--mode` | `smart` | `smart`, `pattern`, or `naive` (see above) |
|
|
130
|
+
| `-k, --sample` | `12` | Band depth to sample from; clamped at detected inner border structure |
|
|
131
|
+
| `--trim` | `auto` | Scanner-bloom lines to cut per edge |
|
|
132
|
+
| `--shuffle` | `48` | How far along the edge texture may be borrowed from |
|
|
133
|
+
| `--noise`, `--smudge` | `0.35`, `0.6` | Added grain (relative to the border's own) and ramped blur |
|
|
134
|
+
| `--seed` | `0` | Output is deterministic per file |
|
|
135
|
+
|
|
136
|
+
## How it works
|
|
137
|
+
|
|
138
|
+
Each edge is analyzed on the original image: bloom lines are trimmed and the
|
|
139
|
+
sampling band is clamped before inner border structure. The border is split
|
|
140
|
+
into a smooth tone component, which is continued outward mirrored so
|
|
141
|
+
gradients stay seam-continuous, and a texture residual, which is resampled
|
|
142
|
+
according to the selected mode. Noise matched to the border's measured grain
|
|
143
|
+
and a ramped blur are applied on top. Corners are filled in two passes so
|
|
144
|
+
they inherit synthesized side texture. All randomness ramps in from zero at
|
|
145
|
+
the seam, so the first synthesized line is an exact continuation of the edge.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
git clone https://github.com/ErikBavenstrand/cardbleed && cd cardbleed
|
|
151
|
+
uv run cardbleed --selfcheck # assertion suite (fixtures)
|
|
152
|
+
uv run cardbleed --selfcheck scan.png # plus checks against a real scan
|
|
153
|
+
uv run --group dev ruff check src
|
|
154
|
+
uv run --group dev pyright
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Module layout: `synthesis.py` (edge analysis and border synthesis),
|
|
158
|
+
`formats.py` (format-preserving I/O, including the JPEG DCT path),
|
|
159
|
+
`sizing.py` (px/mm/target/aspect math), `process.py` (per-file pipeline),
|
|
160
|
+
`cli.py`, `selfcheck.py`.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# cardbleed
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ErikBavenstrand/cardbleed/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/cardbleed/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Extends the borders of card scans so a printed and cut card doesn't end up
|
|
8
|
+
with a border that is too thin. The extension continues whatever border the
|
|
9
|
+
card already has (holofoil speckle, solid colors, gradients), and the
|
|
10
|
+
original image data is never re-encoded: PNG and WebP pixels stay
|
|
11
|
+
bit-identical, and JPEGs are extended by splicing DCT coefficient blocks
|
|
12
|
+
around the untouched originals.
|
|
13
|
+
|
|
14
|
+
<table>
|
|
15
|
+
<tr>
|
|
16
|
+
<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>
|
|
17
|
+
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_smart.png" width="200"><br><sub>output, <code>cardbleed demo_card.png -e 24</code></sub></td>
|
|
18
|
+
</tr>
|
|
19
|
+
</table>
|
|
20
|
+
|
|
21
|
+
The demo card is generated by a script
|
|
22
|
+
([examples/make_demo.py](examples/make_demo.py)), so the repository contains
|
|
23
|
+
no copyrighted scans. It has the traits that make real scans annoying: a
|
|
24
|
+
speckled border, a brightness gradient across it, a scanner-bloom line at the
|
|
25
|
+
very edge, and an inner frame line. Bloom is trimmed and the frame line is
|
|
26
|
+
detected automatically, so sampling never crosses into it.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv tool install cardbleed # or: pipx install cardbleed
|
|
32
|
+
uvx cardbleed card.png # or run once without installing
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Until the first PyPI release, install from GitHub instead:
|
|
36
|
+
`uv tool install git+https://github.com/ErikBavenstrand/cardbleed`
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cardbleed card.png --compare # extend 16px, write a comparison sheet
|
|
42
|
+
cardbleed ./cards/ -e 2.5mm --recursive # batch a folder, mm-based sizing
|
|
43
|
+
cardbleed card.jpg -e 20 --fix-aspect # pad to the 63x88 ratio, then extend
|
|
44
|
+
cardbleed card.png --target 69x94mm # pad to an exact final size
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Outputs are written next to the input (or to `--out-dir`) with an `_ext`
|
|
48
|
+
suffix. Inputs are never overwritten. `--compare` also writes a side-by-side
|
|
49
|
+
sheet with the original boundary marked, which makes it easy to check the
|
|
50
|
+
seam.
|
|
51
|
+
|
|
52
|
+
## Modes
|
|
53
|
+
|
|
54
|
+
Zoomed left-edge detail, one panel per setting: smart, pattern, naive,
|
|
55
|
+
mirror, soft.
|
|
56
|
+
|
|
57
|
+
<img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_detail_modes.png" width="740">
|
|
58
|
+
|
|
59
|
+
- `--mode smart` (default) resamples the border band stochastically. Speckle
|
|
60
|
+
is re-randomized in both directions, so nothing streaks or repeats, at the
|
|
61
|
+
cost of some texture structure.
|
|
62
|
+
- `--mode pattern` keeps structure intact: every output line is a real
|
|
63
|
+
contiguous border line, and each outward pass is shifted along the edge by
|
|
64
|
+
a random offset. If the border has a repeating pattern, detected by
|
|
65
|
+
autocorrelation, the continuation and the offsets snap to its period so the
|
|
66
|
+
pattern stays in phase. This is usually the best choice for holo borders.
|
|
67
|
+
With `--shuffle 0` it degrades to a plain deterministic mirror.
|
|
68
|
+
- `--mode naive` replicates the outermost line straight outward (plus noise
|
|
69
|
+
and smudge). Mostly useful as a baseline; it streaks on textured borders.
|
|
70
|
+
|
|
71
|
+
The gallery variants above, for reference:
|
|
72
|
+
|
|
73
|
+
<table>
|
|
74
|
+
<tr>
|
|
75
|
+
<td align="center"><img src="https://raw.githubusercontent.com/ErikBavenstrand/cardbleed/main/examples/demo_card_pattern.png" width="180"><br><sub><code>--mode pattern</code></sub></td>
|
|
76
|
+
<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>
|
|
77
|
+
<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>
|
|
78
|
+
<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>
|
|
79
|
+
</tr>
|
|
80
|
+
</table>
|
|
81
|
+
|
|
82
|
+
## Format handling
|
|
83
|
+
|
|
84
|
+
| Format | What happens to the original data |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| PNG | re-serialized losslessly; pixels bit-identical |
|
|
87
|
+
| WebP | written as lossless WebP; decoded pixels preserved exactly |
|
|
88
|
+
| 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 |
|
|
89
|
+
|
|
90
|
+
For JPEG the extension amounts have to align to the MCU grid (8 or 16 px).
|
|
91
|
+
The remainder is shifted between opposite edges, so the final dimensions are
|
|
92
|
+
still exactly what you asked for.
|
|
93
|
+
|
|
94
|
+
## Options
|
|
95
|
+
|
|
96
|
+
`cardbleed --help` has the full reference. The ones worth knowing:
|
|
97
|
+
|
|
98
|
+
| Flag | Default | Meaning |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `-e, --extend` | `16` | Amount per edge, px or mm (`2.5mm`); per-edge overrides via `--left` etc. |
|
|
101
|
+
| `--fix-aspect` | off | Pad the short axis to the card ratio (`--card-size`, default `63x88` mm) before extending |
|
|
102
|
+
| `--target` | none | Pad to an exact final size instead, e.g. `69x94mm` |
|
|
103
|
+
| `--mode` | `smart` | `smart`, `pattern`, or `naive` (see above) |
|
|
104
|
+
| `-k, --sample` | `12` | Band depth to sample from; clamped at detected inner border structure |
|
|
105
|
+
| `--trim` | `auto` | Scanner-bloom lines to cut per edge |
|
|
106
|
+
| `--shuffle` | `48` | How far along the edge texture may be borrowed from |
|
|
107
|
+
| `--noise`, `--smudge` | `0.35`, `0.6` | Added grain (relative to the border's own) and ramped blur |
|
|
108
|
+
| `--seed` | `0` | Output is deterministic per file |
|
|
109
|
+
|
|
110
|
+
## How it works
|
|
111
|
+
|
|
112
|
+
Each edge is analyzed on the original image: bloom lines are trimmed and the
|
|
113
|
+
sampling band is clamped before inner border structure. The border is split
|
|
114
|
+
into a smooth tone component, which is continued outward mirrored so
|
|
115
|
+
gradients stay seam-continuous, and a texture residual, which is resampled
|
|
116
|
+
according to the selected mode. Noise matched to the border's measured grain
|
|
117
|
+
and a ramped blur are applied on top. Corners are filled in two passes so
|
|
118
|
+
they inherit synthesized side texture. All randomness ramps in from zero at
|
|
119
|
+
the seam, so the first synthesized line is an exact continuation of the edge.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git clone https://github.com/ErikBavenstrand/cardbleed && cd cardbleed
|
|
125
|
+
uv run cardbleed --selfcheck # assertion suite (fixtures)
|
|
126
|
+
uv run cardbleed --selfcheck scan.png # plus checks against a real scan
|
|
127
|
+
uv run --group dev ruff check src
|
|
128
|
+
uv run --group dev pyright
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Module layout: `synthesis.py` (edge analysis and border synthesis),
|
|
132
|
+
`formats.py` (format-preserving I/O, including the JPEG DCT path),
|
|
133
|
+
`sizing.py` (px/mm/target/aspect math), `process.py` (per-file pipeline),
|
|
134
|
+
`cli.py`, `selfcheck.py`.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
[MIT](LICENSE)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = ["pillow>=10", "numpy>=1.26"]
|
|
4
|
+
# ///
|
|
5
|
+
"""Generate the procedurally-made demo card used in the README.
|
|
6
|
+
|
|
7
|
+
Everything is synthetic — no copyrighted card art or scans. The card mimics
|
|
8
|
+
the traits cardbleed handles: a speckled border with a real inward-darkening
|
|
9
|
+
gradient, a 1px scanner-bloom line at the edge, and a bright inner frame line.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from PIL import Image
|
|
16
|
+
|
|
17
|
+
W, H, BORDER = 400, 550, 14
|
|
18
|
+
rng = np.random.default_rng(42)
|
|
19
|
+
|
|
20
|
+
# border: teal base with an inward-darkening gradient
|
|
21
|
+
card = np.zeros((H, W, 3), np.float32)
|
|
22
|
+
yy, xx = np.mgrid[0:H, 0:W]
|
|
23
|
+
depth = np.minimum.reduce([xx, yy, W - 1 - xx, H - 1 - yy]).astype(np.float32)
|
|
24
|
+
base = np.array([70, 140, 150], np.float32)
|
|
25
|
+
card[:] = base + (28 - np.minimum(depth, BORDER) * 2.0)[..., None]
|
|
26
|
+
|
|
27
|
+
# holo-ish speckle: random bright flecks of varying size and hue
|
|
28
|
+
border_mask = depth < BORDER
|
|
29
|
+
for _ in range(2600):
|
|
30
|
+
y, x = rng.integers(0, H), rng.integers(0, W)
|
|
31
|
+
if not border_mask[y, x]:
|
|
32
|
+
continue
|
|
33
|
+
r = int(rng.integers(1, 3))
|
|
34
|
+
color = rng.uniform(120, 255, 3)
|
|
35
|
+
card[max(0, y - r) : y + r, max(0, x - r) : x + r] += color * rng.uniform(0.3, 0.9)
|
|
36
|
+
|
|
37
|
+
# bright inner frame line, then a plain "art" area with simple shapes
|
|
38
|
+
inner = depth >= BORDER
|
|
39
|
+
card[(depth >= BORDER) & (depth < BORDER + 2)] = (215, 220, 225)
|
|
40
|
+
art = depth >= BORDER + 2
|
|
41
|
+
card[art] = np.array([235, 230, 215]) - (yy[art] / H * 40)[..., None]
|
|
42
|
+
cy, cx = H * 0.42, W * 0.5
|
|
43
|
+
disc = (yy - cy) ** 2 + (xx - cx) ** 2 < 90**2
|
|
44
|
+
card[disc & art] = (200, 120, 90)
|
|
45
|
+
ring = np.abs(np.sqrt((yy - cy) ** 2 + (xx - cx) ** 2) - 120) < 6
|
|
46
|
+
card[ring & art] = (100, 110, 160)
|
|
47
|
+
|
|
48
|
+
# 1px scanner bloom at the very edge (cardbleed auto-trims this)
|
|
49
|
+
card[depth < 1] = np.clip(card[depth < 1] + 70, 0, 255)
|
|
50
|
+
|
|
51
|
+
card += rng.normal(0, 2.5, card.shape) # mild global scan noise
|
|
52
|
+
out = Path(__file__).parent / "demo_card.png"
|
|
53
|
+
Image.fromarray(np.clip(card, 0, 255).astype(np.uint8)).save(out)
|
|
54
|
+
print(f"wrote {out}")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cardbleed"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Extend the borders of card scans for printing — continues the existing border pattern without re-encoding the original image data"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Erik Bävenstrand" }]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pillow>=10",
|
|
12
|
+
"numpy>=1.26",
|
|
13
|
+
"jpeglib>=1.0",
|
|
14
|
+
"rich-click>=1.8",
|
|
15
|
+
]
|
|
16
|
+
keywords = ["card", "proxy", "printing", "bleed", "border", "image", "tcg"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: End Users/Desktop",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
|
26
|
+
"Topic :: Printing",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/ErikBavenstrand/cardbleed"
|
|
31
|
+
Issues = "https://github.com/ErikBavenstrand/cardbleed/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
cardbleed = "cardbleed:cli"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"ruff>=0.8",
|
|
39
|
+
"pyright>=1.1.390",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 88
|
|
44
|
+
src = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
# the multiplication sign is used deliberately in size displays (400×550)
|
|
48
|
+
allowed-confusables = ["×"]
|
|
49
|
+
select = [
|
|
50
|
+
"E", # pycodestyle errors
|
|
51
|
+
"W", # pycodestyle warnings
|
|
52
|
+
"F", # pyflakes
|
|
53
|
+
"I", # isort
|
|
54
|
+
"UP", # pyupgrade
|
|
55
|
+
"B", # bugbear
|
|
56
|
+
"SIM", # simplify
|
|
57
|
+
"RUF", # ruff-specific
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.pyright]
|
|
61
|
+
include = ["src"]
|
|
62
|
+
typeCheckingMode = "standard"
|
|
63
|
+
pythonVersion = "3.11"
|
|
64
|
+
|
|
65
|
+
[build-system]
|
|
66
|
+
requires = ["hatchling"]
|
|
67
|
+
build-backend = "hatchling.build"
|
|
68
|
+
|
|
69
|
+
[tool.hatch.version]
|
|
70
|
+
path = "src/cardbleed/_version.py"
|
|
71
|
+
|
|
72
|
+
[tool.hatch.build.targets.wheel]
|
|
73
|
+
packages = ["src/cardbleed"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Extend the borders of card scans outward for printing.
|
|
2
|
+
|
|
3
|
+
Continues the existing border pattern (holo speckle, solid colors, ...)
|
|
4
|
+
uniformly on all four edges without ever degrading the original image data:
|
|
5
|
+
|
|
6
|
+
PNG -> PNG original pixels bit-identical (lossless re-serialize)
|
|
7
|
+
WebP -> WebP written lossless; decoded original pixels preserved exactly
|
|
8
|
+
JPEG -> JPEG DCT-domain surgery: original coefficient blocks are copied
|
|
9
|
+
bit-exact into a larger grid; only new border blocks are
|
|
10
|
+
encoded (with the original's own quantization tables)
|
|
11
|
+
|
|
12
|
+
Python API (stable surface):
|
|
13
|
+
|
|
14
|
+
from cardbleed import Params, extend_image
|
|
15
|
+
|
|
16
|
+
result = extend_image(arr, (16, 16, 16, 16), Params(),
|
|
17
|
+
np.random.default_rng(0), overwrite=True, notes=[])
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
from .cli import cli
|
|
22
|
+
from .errors import FileError
|
|
23
|
+
from .synthesis import Params, extend_image
|
|
24
|
+
|
|
25
|
+
__all__ = ["FileError", "Params", "__version__", "cli", "extend_image"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|