arkham-utils 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.
- arkham_utils-0.1.0/PKG-INFO +84 -0
- arkham_utils-0.1.0/README.md +66 -0
- arkham_utils-0.1.0/arkham_utils/__init__.py +0 -0
- arkham_utils-0.1.0/arkham_utils/arkhamdb/__init__.py +50 -0
- arkham_utils-0.1.0/arkham_utils/arkhamdb/card.py +61 -0
- arkham_utils-0.1.0/arkham_utils/arkhamdb/constants.py +1 -0
- arkham_utils-0.1.0/arkham_utils/arkhamdb/public_deck.py +38 -0
- arkham_utils-0.1.0/arkham_utils/arkhamdb/set.py +32 -0
- arkham_utils-0.1.0/arkham_utils/card.py +18 -0
- arkham_utils-0.1.0/arkham_utils/octgn/__init__.py +63 -0
- arkham_utils-0.1.0/arkham_utils/octgn/card.py +25 -0
- arkham_utils-0.1.0/arkham_utils/octgn/deck.py +16 -0
- arkham_utils-0.1.0/arkham_utils/octgn/image_db.py +52 -0
- arkham_utils-0.1.0/arkham_utils/octgn/set.py +56 -0
- arkham_utils-0.1.0/arkham_utils/octgn/set_card.py +29 -0
- arkham_utils-0.1.0/arkham_utils/pdf/__init__.py +0 -0
- arkham_utils-0.1.0/arkham_utils/pdf/builder.py +105 -0
- arkham_utils-0.1.0/arkham_utils/pdf/image_reader.py +22 -0
- arkham_utils-0.1.0/arkham_utils/pdf/tiled_image_builder.py +161 -0
- arkham_utils-0.1.0/pyproject.toml +22 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: arkham-utils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Arkham Horror LCG utilities
|
|
5
|
+
Author: Ian Su
|
|
6
|
+
Author-email: iansu1979+github@gmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Dist: Pillow (>=9.5.0,<10.0.0)
|
|
11
|
+
Requires-Dist: PyMuPDF (>=1.24.5,<2.0.0)
|
|
12
|
+
Requires-Dist: fpdf2 (>=2.7.4,<3.0.0)
|
|
13
|
+
Requires-Dist: platformdirs (>=4.3.7,<5.0.0)
|
|
14
|
+
Requires-Dist: requests (>=2.29.0,<3.0.0)
|
|
15
|
+
Requires-Dist: requests-cache (>=1.2.1,<2.0.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Arkham Horror LCG utilities
|
|
19
|
+
|
|
20
|
+
A simple set of utilities to read from [ArkhamDB](https://arkhamdb.com/) and [OCTGN files](https://github.com/GeckoTH/arkham-horror) mainly used to generate printable proxies in PDF format.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pip install arkham-utils
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Using ArkhamDB images
|
|
27
|
+
|
|
28
|
+
Note the images from ArkhamDB have an FFG watermark on them.
|
|
29
|
+
|
|
30
|
+
```py
|
|
31
|
+
from arkham_utils.arkhamdb import db
|
|
32
|
+
from arkham_utils.pdf.builder import PDFBuilder
|
|
33
|
+
|
|
34
|
+
cards = [db.search(name) for name in [
|
|
35
|
+
'Blackjack',
|
|
36
|
+
'Guts',
|
|
37
|
+
'Emergency Cache',
|
|
38
|
+
'Elder Sign',
|
|
39
|
+
'Shrivelling',
|
|
40
|
+
'Daisy Walker'
|
|
41
|
+
]]
|
|
42
|
+
pdf = PDFBuilder(cards)
|
|
43
|
+
pdf.write('proxies.pdf')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Using OCTGN images
|
|
47
|
+
|
|
48
|
+
You will need to first download the image packs from <https://ahlcgoctgn.wordpress.com/image-packs/> into your application support directory (`Library/Application Support/arkham_utils` on MacOS).
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
from arkham_utils.octgn import db
|
|
52
|
+
from arkham_utils.pdf.builder import PDFBuilder
|
|
53
|
+
|
|
54
|
+
cards = [db.find(name) for name in [
|
|
55
|
+
'Blackjack',
|
|
56
|
+
'Guts',
|
|
57
|
+
'Emergency Cache',
|
|
58
|
+
'Elder Sign',
|
|
59
|
+
'Shrivelling',
|
|
60
|
+
'Daisy Walker'
|
|
61
|
+
]]
|
|
62
|
+
pdf = PDFBuilder(cards)
|
|
63
|
+
pdf.write('proxies.pdf')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Proxying an entire set
|
|
67
|
+
|
|
68
|
+
```py
|
|
69
|
+
from arkham_utils.octgn import db
|
|
70
|
+
from arkham_utils.pdf.builder import PDFBuilder, PDFBuilderConfig
|
|
71
|
+
|
|
72
|
+
# write all the non-mini cards as a PDF
|
|
73
|
+
barkham = db.find_set('Meowlathotep')
|
|
74
|
+
PDFBuilder([c for c in barkham.cards if c.type != 'Mini']).write('barkham.pdf')
|
|
75
|
+
|
|
76
|
+
# write the mini investigator cards
|
|
77
|
+
mini_cfg = PDFBuilderConfig()
|
|
78
|
+
mini_cfg.cards_per_row = 4
|
|
79
|
+
mini_cfg.rows_per_page = 4
|
|
80
|
+
mini_cfg.card_height = 2.5
|
|
81
|
+
mini_cfg.card_width = 1.625
|
|
82
|
+
PDFBuilder([c for c in barkham.cards if c.type == 'Mini'], mini_cfg).write('barkham_minis.pdf')
|
|
83
|
+
```
|
|
84
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Arkham Horror LCG utilities
|
|
2
|
+
|
|
3
|
+
A simple set of utilities to read from [ArkhamDB](https://arkhamdb.com/) and [OCTGN files](https://github.com/GeckoTH/arkham-horror) mainly used to generate printable proxies in PDF format.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install arkham-utils
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Using ArkhamDB images
|
|
10
|
+
|
|
11
|
+
Note the images from ArkhamDB have an FFG watermark on them.
|
|
12
|
+
|
|
13
|
+
```py
|
|
14
|
+
from arkham_utils.arkhamdb import db
|
|
15
|
+
from arkham_utils.pdf.builder import PDFBuilder
|
|
16
|
+
|
|
17
|
+
cards = [db.search(name) for name in [
|
|
18
|
+
'Blackjack',
|
|
19
|
+
'Guts',
|
|
20
|
+
'Emergency Cache',
|
|
21
|
+
'Elder Sign',
|
|
22
|
+
'Shrivelling',
|
|
23
|
+
'Daisy Walker'
|
|
24
|
+
]]
|
|
25
|
+
pdf = PDFBuilder(cards)
|
|
26
|
+
pdf.write('proxies.pdf')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Using OCTGN images
|
|
30
|
+
|
|
31
|
+
You will need to first download the image packs from <https://ahlcgoctgn.wordpress.com/image-packs/> into your application support directory (`Library/Application Support/arkham_utils` on MacOS).
|
|
32
|
+
|
|
33
|
+
```py
|
|
34
|
+
from arkham_utils.octgn import db
|
|
35
|
+
from arkham_utils.pdf.builder import PDFBuilder
|
|
36
|
+
|
|
37
|
+
cards = [db.find(name) for name in [
|
|
38
|
+
'Blackjack',
|
|
39
|
+
'Guts',
|
|
40
|
+
'Emergency Cache',
|
|
41
|
+
'Elder Sign',
|
|
42
|
+
'Shrivelling',
|
|
43
|
+
'Daisy Walker'
|
|
44
|
+
]]
|
|
45
|
+
pdf = PDFBuilder(cards)
|
|
46
|
+
pdf.write('proxies.pdf')
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Proxying an entire set
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
from arkham_utils.octgn import db
|
|
53
|
+
from arkham_utils.pdf.builder import PDFBuilder, PDFBuilderConfig
|
|
54
|
+
|
|
55
|
+
# write all the non-mini cards as a PDF
|
|
56
|
+
barkham = db.find_set('Meowlathotep')
|
|
57
|
+
PDFBuilder([c for c in barkham.cards if c.type != 'Mini']).write('barkham.pdf')
|
|
58
|
+
|
|
59
|
+
# write the mini investigator cards
|
|
60
|
+
mini_cfg = PDFBuilderConfig()
|
|
61
|
+
mini_cfg.cards_per_row = 4
|
|
62
|
+
mini_cfg.rows_per_page = 4
|
|
63
|
+
mini_cfg.card_height = 2.5
|
|
64
|
+
mini_cfg.card_width = 1.625
|
|
65
|
+
PDFBuilder([c for c in barkham.cards if c.type == 'Mini'], mini_cfg).write('barkham_minis.pdf')
|
|
66
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Generator
|
|
3
|
+
import requests
|
|
4
|
+
from arkham_utils.arkhamdb.card import ArkhamDBCard
|
|
5
|
+
from arkham_utils.arkhamdb.constants import API
|
|
6
|
+
from arkham_utils.arkhamdb.set import ArkhamDBSet
|
|
7
|
+
import requests_cache
|
|
8
|
+
from platformdirs import user_cache_dir
|
|
9
|
+
|
|
10
|
+
requests_cache.install_cache(
|
|
11
|
+
f'{user_cache_dir('arkham_utils')}/arkhamdb_cache', expire_after=1800)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ArkhamDB(object):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
print('loading packs', f'{API}/packs/')
|
|
17
|
+
r = requests.get(f'{API}/packs/')
|
|
18
|
+
self.packs_data = r.json()
|
|
19
|
+
self.packs = [ArkhamDBSet(pack) for pack in self.packs_data]
|
|
20
|
+
print(f'loaded {len(self.packs)} packs')
|
|
21
|
+
|
|
22
|
+
def find_all_sets(self, regex: str) -> Generator[ArkhamDBSet, None, None]:
|
|
23
|
+
for s in self.packs:
|
|
24
|
+
m = re.search(regex, s.name)
|
|
25
|
+
if m:
|
|
26
|
+
yield s
|
|
27
|
+
|
|
28
|
+
def find_set(self, regex: str) -> ArkhamDBSet:
|
|
29
|
+
return next(self.find_all_sets(regex))
|
|
30
|
+
|
|
31
|
+
# beware that this will return Revised Core and Core together
|
|
32
|
+
def find_sets_in_cycle(self, regex: str) -> Generator[ArkhamDBSet, None, None]:
|
|
33
|
+
root = self.find_set(regex)
|
|
34
|
+
cycle = root.data['cycle_position']
|
|
35
|
+
for s in self.packs:
|
|
36
|
+
if s.data['cycle_position'] == cycle:
|
|
37
|
+
yield s
|
|
38
|
+
|
|
39
|
+
def find_all_cards(self, regex: str) -> Generator[ArkhamDBCard, None, None]:
|
|
40
|
+
for s in self.packs:
|
|
41
|
+
for c in s.cards:
|
|
42
|
+
m = re.search(regex, c.name)
|
|
43
|
+
if m:
|
|
44
|
+
yield c
|
|
45
|
+
|
|
46
|
+
def search(self, regex: str) -> ArkhamDBCard:
|
|
47
|
+
return next(self.find_all_cards(regex))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
db = ArkhamDB()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from arkham_utils.card import ArkhamCard
|
|
3
|
+
from PIL import Image
|
|
4
|
+
import io
|
|
5
|
+
import requests
|
|
6
|
+
from arkham_utils.arkhamdb.constants import API
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_image_from_url(url) -> Image.Image:
|
|
10
|
+
r = requests.get(url)
|
|
11
|
+
return Image.open(io.BytesIO(r.content))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ArkhamDBCard(ArkhamCard):
|
|
15
|
+
def __init__(self, data):
|
|
16
|
+
self.data = data
|
|
17
|
+
self._images = None
|
|
18
|
+
|
|
19
|
+
def _lazy_load_images(self):
|
|
20
|
+
if self._images is not None:
|
|
21
|
+
return
|
|
22
|
+
if 'backimagesrc' in self.data:
|
|
23
|
+
self._images = _get_image_from_url(f'https://arkhamdb.com{self.data["imagesrc"]}'), _get_image_from_url(
|
|
24
|
+
f'https://arkhamdb.com{self.data["backimagesrc"]}')
|
|
25
|
+
elif 'imagesrc' in self.data:
|
|
26
|
+
self._images = _get_image_from_url(
|
|
27
|
+
f'https://arkhamdb.com{self.data["imagesrc"]}'),
|
|
28
|
+
else:
|
|
29
|
+
self._images = []
|
|
30
|
+
print(f"{self.name} has no images!")
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def code(self):
|
|
34
|
+
return self.data['code']
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def name(self):
|
|
38
|
+
return self.data['name']
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def image(self):
|
|
42
|
+
self._lazy_load_images()
|
|
43
|
+
return self._images[0]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def faction(self):
|
|
47
|
+
return self.data['faction_code']
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def type(self):
|
|
51
|
+
return self.data['type_code']
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def images(self):
|
|
55
|
+
self._lazy_load_images()
|
|
56
|
+
return self._images
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def from_id(id: str):
|
|
60
|
+
r = requests.get(f"{API}/card/{id}.json")
|
|
61
|
+
return ArkhamDBCard(r.json())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
API = 'https://arkhamdb.com/api/public'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
from arkham_utils.arkhamdb.card import ArkhamDBCard
|
|
3
|
+
from arkham_utils.arkhamdb.constants import API
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ArkhamDBPublicDeck(object):
|
|
8
|
+
id: int
|
|
9
|
+
data: object
|
|
10
|
+
_cards: Dict[str, ArkhamDBCard]
|
|
11
|
+
|
|
12
|
+
def __init__(self, id: int, is_decklist: bool = False):
|
|
13
|
+
self.id = id
|
|
14
|
+
key = 'decklist' if is_decklist else 'deck'
|
|
15
|
+
r = requests.get(f"{API}/{key}/{self.id}.json")
|
|
16
|
+
print(f"{API}/{key}/{self.id}.json")
|
|
17
|
+
print(r.content)
|
|
18
|
+
self.data = r.json()
|
|
19
|
+
self._cards = None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def cards(self):
|
|
23
|
+
if self._cards is None:
|
|
24
|
+
self._cards = {}
|
|
25
|
+
for id in self.data['slots']:
|
|
26
|
+
self._cards[id] = ArkhamDBCard.from_id(id)
|
|
27
|
+
|
|
28
|
+
all_cards = []
|
|
29
|
+
for id, count in self.data['slots'].items():
|
|
30
|
+
all_cards.extend([self._cards[id]] * count)
|
|
31
|
+
|
|
32
|
+
return all_cards
|
|
33
|
+
|
|
34
|
+
def dump(self):
|
|
35
|
+
self.cards
|
|
36
|
+
print(f"{self.data['name']}")
|
|
37
|
+
for id, count in self.data['slots'].items():
|
|
38
|
+
print(f" {self._cards[id].name} x{count}")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
import requests
|
|
3
|
+
from arkham_utils.arkhamdb.card import ArkhamDBCard
|
|
4
|
+
from arkham_utils.arkhamdb.constants import API
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
class ArkhamDBSet(object):
|
|
8
|
+
def __init__(self, data):
|
|
9
|
+
self.data = data
|
|
10
|
+
self._cards = None
|
|
11
|
+
|
|
12
|
+
def _lazy_load_cards(self, include_encounter=True):
|
|
13
|
+
if self._cards is not None:
|
|
14
|
+
return
|
|
15
|
+
r = requests.get(
|
|
16
|
+
f"{API}/cards/{self.data['code']}?encounter={1 if include_encounter else 0}")
|
|
17
|
+
cards_data = r.json()
|
|
18
|
+
self._cards = [ArkhamDBCard(data) for data in cards_data]
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self):
|
|
22
|
+
return self.data['name']
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def cards(self):
|
|
26
|
+
self._lazy_load_cards()
|
|
27
|
+
return self._cards
|
|
28
|
+
|
|
29
|
+
def find_by_regex(self, regex: str | re.Pattern[str]) -> Generator[ArkhamDBCard, None, None]:
|
|
30
|
+
for c in self.cards:
|
|
31
|
+
if re.match(regex, c.name):
|
|
32
|
+
yield c
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC,abstractmethod
|
|
2
|
+
|
|
3
|
+
class ArkhamCard(ABC):
|
|
4
|
+
def __init__(self, front, back=None):
|
|
5
|
+
self._images = (front, back) if back else front,
|
|
6
|
+
|
|
7
|
+
@property
|
|
8
|
+
def image(self):
|
|
9
|
+
return self._images[0]
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def images(self):
|
|
13
|
+
return self._images
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def type(self):
|
|
18
|
+
return 'Card'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from typing import List
|
|
5
|
+
import zipfile
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from arkham_utils.octgn.set import OctgnSet
|
|
11
|
+
from platformdirs import user_data_dir
|
|
12
|
+
|
|
13
|
+
class OctgnSetDatabase(object):
|
|
14
|
+
sets: List[OctgnSet]
|
|
15
|
+
path: str
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def gitzip(self):
|
|
19
|
+
return f'{self.path}/o8g.zip'
|
|
20
|
+
|
|
21
|
+
def download(self):
|
|
22
|
+
if os.path.exists(self.gitzip) and os.path.getmtime(self.gitzip) > time.time() - 60 * 60 * 24:
|
|
23
|
+
return
|
|
24
|
+
os.makedirs(self.path, exist_ok=True)
|
|
25
|
+
r = requests.get('https://github.com/GeckoTH/arkham-horror/archive/refs/heads/master.zip', allow_redirects=True)
|
|
26
|
+
open(self.gitzip, 'wb').write(r.content)
|
|
27
|
+
|
|
28
|
+
def __init__(self, path='o8g'):
|
|
29
|
+
self.path = path
|
|
30
|
+
self.sets = []
|
|
31
|
+
self.download()
|
|
32
|
+
with zipfile.ZipFile(self.gitzip) as z:
|
|
33
|
+
for f in z.filelist:
|
|
34
|
+
if f.filename.endswith('/set.xml'):
|
|
35
|
+
with z.open(f.filename) as fd:
|
|
36
|
+
s = OctgnSet(fd)
|
|
37
|
+
self.sets.append(s)
|
|
38
|
+
print(f'loaded {len(self.sets)} sets, {sum([len(s.cards) for s in self.sets])} cards')
|
|
39
|
+
|
|
40
|
+
def lookup(self, card_id):
|
|
41
|
+
for s in self.sets:
|
|
42
|
+
card = s.find(card_id)
|
|
43
|
+
if card is not None:
|
|
44
|
+
return card
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def find_all(self, regex, xp=None):
|
|
48
|
+
for s in self.sets:
|
|
49
|
+
for found in s.find_by_regex(regex, xp):
|
|
50
|
+
yield found
|
|
51
|
+
|
|
52
|
+
def find(self, regex, xp=None):
|
|
53
|
+
return next(self.find_all(regex, xp))
|
|
54
|
+
|
|
55
|
+
def find_all_sets(self, regex):
|
|
56
|
+
for s in self.sets:
|
|
57
|
+
if re.search(regex, s.name):
|
|
58
|
+
yield s
|
|
59
|
+
|
|
60
|
+
def find_set(self, regex):
|
|
61
|
+
return next(self.find_all_sets(regex))
|
|
62
|
+
|
|
63
|
+
db = OctgnSetDatabase(f'{user_data_dir('arkham_utils')}/o8g')
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from xml.etree.ElementTree import Element
|
|
2
|
+
from arkham_utils.card import ArkhamCard
|
|
3
|
+
from arkham_utils.octgn.image_db import image_db
|
|
4
|
+
|
|
5
|
+
class OctgnCard(ArkhamCard):
|
|
6
|
+
def __init__(self, card_elem: Element, image_db=image_db):
|
|
7
|
+
self.elem = card_elem
|
|
8
|
+
self.image_db = image_db
|
|
9
|
+
@property
|
|
10
|
+
def id(self):
|
|
11
|
+
return self.elem.attrib['id']
|
|
12
|
+
@property
|
|
13
|
+
def name(self):
|
|
14
|
+
return self.elem.text
|
|
15
|
+
@property
|
|
16
|
+
def image(self):
|
|
17
|
+
return self.image_db.get_image(self.id)
|
|
18
|
+
@property
|
|
19
|
+
def images(self):
|
|
20
|
+
return self.image_db.get_images(self.id)
|
|
21
|
+
@property
|
|
22
|
+
def quantity(self):
|
|
23
|
+
return int(self.elem.attrib['qty'])
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return f"[{self.id}] {self.name}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import sys
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
|
|
5
|
+
from arkham_utils.octgn.card import OctgnCard
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OctgnDeck(object):
|
|
9
|
+
def __init__(self, o8d='starter-deck.o8d'):
|
|
10
|
+
tree = ET.parse(o8d)
|
|
11
|
+
self.cards = [OctgnCard(card) for card in tree.findall('.//card')]
|
|
12
|
+
def csv(self, fh=sys.stdout):
|
|
13
|
+
writer = csv.writer(fh)
|
|
14
|
+
for card in self.cards:
|
|
15
|
+
writer.writerow([card.id, card.name, card.quantity])
|
|
16
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import zipfile
|
|
3
|
+
import re
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from platformdirs import user_data_dir
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
# download files from https://ahlcgoctgn.wordpress.com/image-packs/ into o8c/
|
|
9
|
+
class OctgnImageDatabase(object):
|
|
10
|
+
def __init__(self, path='o8c'):
|
|
11
|
+
if not os.path.exists(path):
|
|
12
|
+
raise RuntimeError(f'{path} does not exist! Please download the image packs from https://ahlcgoctgn.wordpress.com/image-packs/ into {path}')
|
|
13
|
+
|
|
14
|
+
self.images = {} # uuid -> (zip_name, zip_internal_file)
|
|
15
|
+
self.images_back = {}
|
|
16
|
+
for o8c in glob.glob(f'{path}/*.o8c'):
|
|
17
|
+
with zipfile.ZipFile(o8c) as z:
|
|
18
|
+
for f in z.filelist:
|
|
19
|
+
m = re.match(
|
|
20
|
+
r'[^/]+/Sets/[^/]+/Cards/([^\.]+)\.(jpg|png)$', f.filename)
|
|
21
|
+
if m is not None:
|
|
22
|
+
uuid = m.group(1)
|
|
23
|
+
self.images[uuid] = (o8c, f.filename)
|
|
24
|
+
else:
|
|
25
|
+
m = re.match(
|
|
26
|
+
r'[^/]+/Sets/[^/]+/Cards/([^\.]+)\.[A-Za-z]\.(jpg|png)$', f.filename)
|
|
27
|
+
if m is not None:
|
|
28
|
+
uuid = m.group(1)
|
|
29
|
+
self.images_back[uuid] = (o8c, f.filename)
|
|
30
|
+
|
|
31
|
+
print(f'loaded {len(self.images)} card images and {len(self.images_back)} back images')
|
|
32
|
+
|
|
33
|
+
def get_image(self, card_id, front=True):
|
|
34
|
+
images = self.images if front else self.images_back
|
|
35
|
+
if card_id not in images:
|
|
36
|
+
raise KeyError('unknown card_id', card_id)
|
|
37
|
+
o8c, fname = images[card_id]
|
|
38
|
+
with zipfile.ZipFile(o8c) as z:
|
|
39
|
+
with z.open(fname) as f:
|
|
40
|
+
i = Image.open(f)
|
|
41
|
+
i.load()
|
|
42
|
+
return i
|
|
43
|
+
|
|
44
|
+
def get_images(self, card_id):
|
|
45
|
+
front = self.get_image(card_id)
|
|
46
|
+
try:
|
|
47
|
+
back = self.get_image(card_id, False)
|
|
48
|
+
return front, back
|
|
49
|
+
except KeyError:
|
|
50
|
+
return front,
|
|
51
|
+
|
|
52
|
+
image_db = OctgnImageDatabase(f'{user_data_dir('arkham_utils')}/o8c')
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from typing import List
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
|
|
7
|
+
from arkham_utils.octgn.set_card import OctgnSetCard
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# check out repo https://github.com/GeckoTH/arkham-horror/tree/master/o8g
|
|
11
|
+
class OctgnSet(object):
|
|
12
|
+
cards: List[OctgnSetCard]
|
|
13
|
+
|
|
14
|
+
def __init__(self, set_xml='o8g/Sets/Core Set/set.xml'):
|
|
15
|
+
self.xml = set_xml
|
|
16
|
+
tree = ET.parse(set_xml)
|
|
17
|
+
self.name = tree.getroot().attrib['name']
|
|
18
|
+
self.cards = [OctgnSetCard(card) for card in tree.findall('.//card')]
|
|
19
|
+
self.counts = {}
|
|
20
|
+
for card in self.cards:
|
|
21
|
+
try:
|
|
22
|
+
self.counts[card.id] = card.quantity
|
|
23
|
+
except AttributeError:
|
|
24
|
+
self.counts[card.id] = 1
|
|
25
|
+
# logging.warning(f'{set_xml} card {card.name} ({card.id}) has no quantity')
|
|
26
|
+
self.proxies = []
|
|
27
|
+
|
|
28
|
+
def csv(self, fh=sys.stdout):
|
|
29
|
+
writer = csv.writer(fh)
|
|
30
|
+
for card in self.cards:
|
|
31
|
+
writer.writerow([card.id, card.set_number, card.name, card.quantity, card.xp])
|
|
32
|
+
|
|
33
|
+
def find(self, card_id):
|
|
34
|
+
return next((x for x in self.cards if x.id == card_id), None)
|
|
35
|
+
|
|
36
|
+
def find_by_regex(self, pattern, xp=None):
|
|
37
|
+
return [x for x in self.cards if re.search(pattern, x.name) is not None and (xp is None or xp == x.xp)]
|
|
38
|
+
|
|
39
|
+
def remove(self, card_id):
|
|
40
|
+
if card_id in self.counts:
|
|
41
|
+
self.counts[card_id] -= 1
|
|
42
|
+
# print(f'ok removing {self.find(card_id).name}, {self.counts[card_id]} left')
|
|
43
|
+
if self.counts[card_id] == 0:
|
|
44
|
+
del self.counts[card_id]
|
|
45
|
+
return True
|
|
46
|
+
if not self.find(card_id):
|
|
47
|
+
raise KeyError(f'Unknown card id: {card_id}')
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def remove_deck(self, deck):
|
|
51
|
+
for card in deck.cards:
|
|
52
|
+
for _ in range(card.quantity):
|
|
53
|
+
ok = self.remove(card.id)
|
|
54
|
+
if not ok:
|
|
55
|
+
# print(f'proxy: {card.name}')
|
|
56
|
+
self.proxies.append(card)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from arkham_utils.octgn.card import OctgnCard
|
|
2
|
+
|
|
3
|
+
class OctgnSetCard(OctgnCard):
|
|
4
|
+
def get_property(self, key):
|
|
5
|
+
return self.elem.find(f'property[@name="{key}"]').attrib['value']
|
|
6
|
+
@property
|
|
7
|
+
def name(self):
|
|
8
|
+
return self.elem.attrib['name']
|
|
9
|
+
@property
|
|
10
|
+
def size(self):
|
|
11
|
+
try:
|
|
12
|
+
return self.elem.attrib['size']
|
|
13
|
+
except:
|
|
14
|
+
return None
|
|
15
|
+
@property
|
|
16
|
+
def set_number(self):
|
|
17
|
+
return int(self.get_property("Card Number"))
|
|
18
|
+
@property
|
|
19
|
+
def quantity(self):
|
|
20
|
+
return int(self.get_property("Quantity"))
|
|
21
|
+
@property
|
|
22
|
+
def type(self):
|
|
23
|
+
return self.get_property("Type")
|
|
24
|
+
@property
|
|
25
|
+
def xp(self):
|
|
26
|
+
try:
|
|
27
|
+
return int(self.get_property("Level"))
|
|
28
|
+
except:
|
|
29
|
+
return 'n/a'
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from typing import Dict, List, Tuple
|
|
2
|
+
|
|
3
|
+
from fpdf import FPDF
|
|
4
|
+
|
|
5
|
+
from arkham_utils.card import ArkhamCard
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MAX_PAGES = 100
|
|
10
|
+
card_width = 2.4
|
|
11
|
+
card_height = 3.5
|
|
12
|
+
cards_per_row = 3
|
|
13
|
+
rows_per_page = 3
|
|
14
|
+
a4_width = 8.27
|
|
15
|
+
a4_height = 11.69
|
|
16
|
+
bleed_thickness = 0.05
|
|
17
|
+
padding = 0.015
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PDFBuilderConfig(object):
|
|
21
|
+
card_width: float = card_width
|
|
22
|
+
card_height: float = card_height
|
|
23
|
+
cards_per_row: int = cards_per_row
|
|
24
|
+
rows_per_page: int = rows_per_page
|
|
25
|
+
bleed_thickness: float = bleed_thickness
|
|
26
|
+
paper_width: float = a4_width
|
|
27
|
+
paper_height: float = a4_height
|
|
28
|
+
padding: float = padding
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PDFBuilder(object):
|
|
32
|
+
def __init__(self, cards: List[ArkhamCard], config: PDFBuilderConfig = PDFBuilderConfig()):
|
|
33
|
+
self.cards = cards
|
|
34
|
+
self.config = config
|
|
35
|
+
self.layout = self.get_layout()
|
|
36
|
+
|
|
37
|
+
def get_layout(self) -> Dict[Tuple[int, int, int], Image.Image]:
|
|
38
|
+
layout = {}
|
|
39
|
+
twosided = [c for c in self.cards if len(c.images) == 2]
|
|
40
|
+
onesided = [c for c in self.cards if len(c.images) == 1]
|
|
41
|
+
for card in twosided:
|
|
42
|
+
self.layout_2side(layout, card)
|
|
43
|
+
for card in onesided:
|
|
44
|
+
self.layout_1side(layout, card)
|
|
45
|
+
return layout
|
|
46
|
+
|
|
47
|
+
def layout_2side(self, layout, card: ArkhamCard):
|
|
48
|
+
front, back = card.images
|
|
49
|
+
if front.width < front.height:
|
|
50
|
+
front = front.rotate(180)
|
|
51
|
+
for p in range(MAX_PAGES):
|
|
52
|
+
for r in range(0, self.config.rows_per_page, 2):
|
|
53
|
+
if r + 1 == self.config.rows_per_page:
|
|
54
|
+
continue
|
|
55
|
+
for c in range(self.config.cards_per_row):
|
|
56
|
+
if (p, r, c) not in layout:
|
|
57
|
+
layout[(p, r, c)] = back
|
|
58
|
+
layout[(p, r + 1, c)] = front
|
|
59
|
+
return
|
|
60
|
+
raise RuntimeError('Exceeded last page')
|
|
61
|
+
|
|
62
|
+
def layout_1side(self, layout, card: ArkhamCard):
|
|
63
|
+
for p in range(MAX_PAGES):
|
|
64
|
+
for r in range(self.config.rows_per_page):
|
|
65
|
+
for c in range(self.config.cards_per_row):
|
|
66
|
+
if (p, r, c) not in layout:
|
|
67
|
+
layout[(p, r, c)] = card.image
|
|
68
|
+
return
|
|
69
|
+
raise RuntimeError('Exceeded last page')
|
|
70
|
+
|
|
71
|
+
def write(self, filename: str):
|
|
72
|
+
padding_left = (self.config.paper_width -
|
|
73
|
+
(self.config.cards_per_row * self.config.card_width) - ((self.config.cards_per_row - 1) * self.config.padding)) / 2
|
|
74
|
+
padding_top = (self.config.paper_height - (self.config.rows_per_page *
|
|
75
|
+
self.config.card_height) - ((self.config.rows_per_page - 1) * self.config.padding)) / 2
|
|
76
|
+
|
|
77
|
+
pdf = FPDF(orientation='P', format='A4', unit='in')
|
|
78
|
+
pages = max([p for (p, r, c) in self.layout]) + 1
|
|
79
|
+
for p in range(pages):
|
|
80
|
+
pdf.add_page()
|
|
81
|
+
# add bleed bg
|
|
82
|
+
for row in range(self.config.rows_per_page):
|
|
83
|
+
for col in range(self.config.cards_per_row):
|
|
84
|
+
if (p, row, col) in self.layout:
|
|
85
|
+
pdf.rect(w=self.config.card_width + 2 * self.config.bleed_thickness,
|
|
86
|
+
h=self.config.card_height + 2 * self.config.bleed_thickness,
|
|
87
|
+
x=padding_left + col *
|
|
88
|
+
(self.config.card_width + self.config.padding) -
|
|
89
|
+
self.config.bleed_thickness,
|
|
90
|
+
y=padding_top + row *
|
|
91
|
+
(self.config.card_height + self.config.padding) -
|
|
92
|
+
self.config.bleed_thickness,
|
|
93
|
+
style='F')
|
|
94
|
+
# add actual images
|
|
95
|
+
for row in range(self.config.rows_per_page):
|
|
96
|
+
for col in range(self.config.cards_per_row):
|
|
97
|
+
if (p, row, col) in self.layout:
|
|
98
|
+
image = self.layout[(p, row, col)]
|
|
99
|
+
if image.width > image.height:
|
|
100
|
+
image = image.rotate(-90, expand=True)
|
|
101
|
+
pdf.image(image, w=self.config.card_width, h=self.config.card_height, x=padding_left +
|
|
102
|
+
col * (self.config.card_width +
|
|
103
|
+
self.config.padding),
|
|
104
|
+
y=padding_top + row * (self.config.card_height + self.config.padding))
|
|
105
|
+
pdf.output(filename)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
from arkham_utils.card import ArkhamCard
|
|
3
|
+
from arkham_utils.pdf.builder import PDFBuilder
|
|
4
|
+
import fitz
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PDFImageReader(object):
|
|
9
|
+
def __init__(self, filename, zoom=4):
|
|
10
|
+
self.filename = filename
|
|
11
|
+
self.zoom = zoom
|
|
12
|
+
|
|
13
|
+
def get_images(self) -> Generator[Image.Image, None, None]:
|
|
14
|
+
with fitz.open(self.filename) as pdf:
|
|
15
|
+
mat = fitz.Matrix(self.zoom, self.zoom)
|
|
16
|
+
for i in range(len(pdf)):
|
|
17
|
+
page = pdf.load_page(i)
|
|
18
|
+
pix = page.get_pixmap(matrix=mat)
|
|
19
|
+
yield Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
|
20
|
+
|
|
21
|
+
def build_pdf(self):
|
|
22
|
+
return PDFBuilder([ArkhamCard(im) for im in self.get_images()])
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
from fpdf import FPDF
|
|
5
|
+
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
a4_width = 210
|
|
9
|
+
a4_height = 297
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PDFTiledImageBuilder(object):
|
|
13
|
+
def __init__(self, image_width_mm, image_height_mm, image_files: List[str], bleed_color: Tuple[int, int, int] = (0, 0, 0), space_between: int = 0.2):
|
|
14
|
+
self.image_width = image_width_mm
|
|
15
|
+
self.image_height = image_height_mm
|
|
16
|
+
self.images = [Image.open(fname) for fname in image_files]
|
|
17
|
+
self.bleed_thickness = 0.5
|
|
18
|
+
self.minimum_page_padding = 5
|
|
19
|
+
self.bleed_color = bleed_color # rgb
|
|
20
|
+
self.space_between = space_between
|
|
21
|
+
|
|
22
|
+
def write(self, filename: str):
|
|
23
|
+
# work out which orientation maximizes tiling
|
|
24
|
+
printable_width = a4_width - 2 * \
|
|
25
|
+
(self.bleed_thickness + self.minimum_page_padding)
|
|
26
|
+
printable_height = a4_height - 2 * \
|
|
27
|
+
(self.bleed_thickness + self.minimum_page_padding)
|
|
28
|
+
rows0 = int(printable_height //
|
|
29
|
+
(self.image_height + self.space_between))
|
|
30
|
+
cols0 = int(printable_width // (self.image_width + self.space_between))
|
|
31
|
+
tiles_per_page0 = rows0 * cols0
|
|
32
|
+
rows1 = int(printable_height //
|
|
33
|
+
(self.image_width + self.space_between))
|
|
34
|
+
cols1 = int(printable_width //
|
|
35
|
+
(self.image_height + self.space_between))
|
|
36
|
+
tiles_per_page1 = rows1 * cols1
|
|
37
|
+
|
|
38
|
+
self.rotate_90deg = tiles_per_page1 > tiles_per_page0
|
|
39
|
+
if not self.rotate_90deg:
|
|
40
|
+
self.rows_per_page = rows0
|
|
41
|
+
self.cols_per_page = cols0
|
|
42
|
+
self.tiles_per_page = tiles_per_page0
|
|
43
|
+
self.tile_width = self.image_width
|
|
44
|
+
self.tile_height = self.image_height
|
|
45
|
+
self.pages = math.ceil(len(self.images) / tiles_per_page0)
|
|
46
|
+
else:
|
|
47
|
+
self.rows_per_page = rows1
|
|
48
|
+
self.cols_per_page = cols1
|
|
49
|
+
self.tiles_per_page = tiles_per_page1
|
|
50
|
+
self.tile_width = self.image_height
|
|
51
|
+
self.tile_height = self.image_width
|
|
52
|
+
self.pages = math.ceil(len(self.images) / tiles_per_page1)
|
|
53
|
+
print(
|
|
54
|
+
f"max tiles_per_page (rotated? {self.rotate_90deg}):{self.tiles_per_page} ({self.cols_per_page}x{self.rows_per_page}) pages:{self.pages}")
|
|
55
|
+
|
|
56
|
+
# work out padding
|
|
57
|
+
self.padding_left = (a4_width - self.cols_per_page * self.tile_width -
|
|
58
|
+
(self.cols_per_page - 1) * self.space_between) / 2
|
|
59
|
+
self.padding_top = (a4_height - self.rows_per_page * self.tile_height -
|
|
60
|
+
(self.rows_per_page - 1) * self.space_between) / 2
|
|
61
|
+
print(f"padding left:{self.padding_left} top:{self.padding_top}")
|
|
62
|
+
|
|
63
|
+
pdf = FPDF(orientation='P', format='A4', unit='mm')
|
|
64
|
+
for p in range(self.pages):
|
|
65
|
+
pdf.add_page()
|
|
66
|
+
# add bleed bg if needed
|
|
67
|
+
if self.bleed_thickness > 0:
|
|
68
|
+
for row in range(self.rows_per_page):
|
|
69
|
+
for col in range(self.cols_per_page):
|
|
70
|
+
idx = p * self.tiles_per_page + row * self.cols_per_page + col
|
|
71
|
+
if idx < len(self.images):
|
|
72
|
+
pdf.set_fill_color(*self.bleed_color)
|
|
73
|
+
pdf.rect(w=self.tile_width + 2 * self.bleed_thickness,
|
|
74
|
+
h=self.tile_height + 2 * self.bleed_thickness,
|
|
75
|
+
x=self.padding_left + col *
|
|
76
|
+
(self.tile_width + self.space_between) -
|
|
77
|
+
self.bleed_thickness,
|
|
78
|
+
y=self.padding_top + row *
|
|
79
|
+
(self.tile_height + self.space_between) -
|
|
80
|
+
self.bleed_thickness,
|
|
81
|
+
style='F')
|
|
82
|
+
# add images
|
|
83
|
+
for row in range(self.rows_per_page):
|
|
84
|
+
for col in range(self.cols_per_page):
|
|
85
|
+
idx = p * self.tiles_per_page + row * self.cols_per_page + col
|
|
86
|
+
if idx < len(self.images):
|
|
87
|
+
print('adding image', idx + 1)
|
|
88
|
+
img = self.images[idx]
|
|
89
|
+
if self.rotate_90deg:
|
|
90
|
+
img = img.rotate(-90, expand=True)
|
|
91
|
+
pdf.image(img, w=self.tile_width, h=self.tile_height, x=self.padding_left +
|
|
92
|
+
col * (self.tile_width + self.space_between), y=self.padding_top + row * (self.tile_height + self.space_between))
|
|
93
|
+
|
|
94
|
+
# # add bleed bg
|
|
95
|
+
# for row in range(3):
|
|
96
|
+
# for col in range(3):
|
|
97
|
+
# if (p, row, col) in self.layout:
|
|
98
|
+
# image = self.layout[(p, row, col)]
|
|
99
|
+
# if image.width > image.height:
|
|
100
|
+
# image = image.rotate(-90, expand=True)
|
|
101
|
+
# pdf.image(image, w=card_width, h=card_height, x=padding_left +
|
|
102
|
+
# col * card_width, y=padding_top + row * card_height)
|
|
103
|
+
pdf.output(filename)
|
|
104
|
+
|
|
105
|
+
# def get_layout(self) -> Dict[Tuple[int, int, int], Image.Image]:
|
|
106
|
+
# layout = {}
|
|
107
|
+
# twosided = [c for c in self.cards if len(c.images) == 2]
|
|
108
|
+
# onesided = [c for c in self.cards if len(c.images) == 1]
|
|
109
|
+
# for card in twosided:
|
|
110
|
+
# self.layout_2side(layout, card)
|
|
111
|
+
# for card in onesided:
|
|
112
|
+
# self.layout_1side(layout, card)
|
|
113
|
+
# return layout
|
|
114
|
+
|
|
115
|
+
# @staticmethod
|
|
116
|
+
# def layout_2side(layout, card: ArkhamCard):
|
|
117
|
+
# front, back = card.images
|
|
118
|
+
# if front.width < front.height:
|
|
119
|
+
# front = front.rotate(180)
|
|
120
|
+
# for p in range(MAX_PAGES):
|
|
121
|
+
# for c in range(3):
|
|
122
|
+
# if (p, 0, c) not in layout:
|
|
123
|
+
# layout[(p, 0, c)] = back
|
|
124
|
+
# layout[(p, 1, c)] = front
|
|
125
|
+
# return
|
|
126
|
+
# raise RuntimeError('Exceeded last page')
|
|
127
|
+
|
|
128
|
+
# @staticmethod
|
|
129
|
+
# def layout_1side(layout, card: ArkhamCard):
|
|
130
|
+
# for p in range(MAX_PAGES):
|
|
131
|
+
# for r in range(3):
|
|
132
|
+
# for c in range(3):
|
|
133
|
+
# if (p, r, c) not in layout:
|
|
134
|
+
# layout[(p, r, c)] = card.image
|
|
135
|
+
# return
|
|
136
|
+
# raise RuntimeError('Exceeded last page')
|
|
137
|
+
|
|
138
|
+
# def write(self, filename: str):
|
|
139
|
+
# pdf = FPDF(orientation='P', format='A4', unit='in')
|
|
140
|
+
# pages = max([p for (p, r, c) in self.layout]) + 1
|
|
141
|
+
# for p in range(pages):
|
|
142
|
+
# pdf.add_page()
|
|
143
|
+
# # add bleed bg
|
|
144
|
+
# for row in range(3):
|
|
145
|
+
# for col in range(3):
|
|
146
|
+
# if (p, row, col) in self.layout:
|
|
147
|
+
# pdf.rect(w=card_width + 2 * bleed_thickness,
|
|
148
|
+
# h=card_height + 2 * bleed_thickness,
|
|
149
|
+
# x=padding_left + col * card_width - bleed_thickness,
|
|
150
|
+
# y=padding_top + row * card_height - bleed_thickness,
|
|
151
|
+
# style='F')
|
|
152
|
+
# # add bleed bg
|
|
153
|
+
# for row in range(3):
|
|
154
|
+
# for col in range(3):
|
|
155
|
+
# if (p, row, col) in self.layout:
|
|
156
|
+
# image = self.layout[(p, row, col)]
|
|
157
|
+
# if image.width > image.height:
|
|
158
|
+
# image = image.rotate(-90, expand=True)
|
|
159
|
+
# pdf.image(image, w=card_width, h=card_height, x=padding_left +
|
|
160
|
+
# col * card_width, y=padding_top + row * card_height)
|
|
161
|
+
# pdf.output(filename)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "arkham-utils"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Arkham Horror LCG utilities"
|
|
5
|
+
authors = ["Ian Su <iansu1979+github@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.12"
|
|
10
|
+
requests = "^2.29.0"
|
|
11
|
+
Pillow = "^9.5.0"
|
|
12
|
+
fpdf2 = "^2.7.4"
|
|
13
|
+
PyMuPDF = "^1.24.5"
|
|
14
|
+
requests-cache = "^1.2.1"
|
|
15
|
+
platformdirs = "^4.3.7"
|
|
16
|
+
|
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
|
18
|
+
pytest = "^8.3.5"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|