mirror of
https://github.com/ldericher/fftcgtool
synced 2025-01-15 15:02:59 +00:00
Begin rewrite
This commit is contained in:
parent
cc5955865b
commit
cd115e2f9e
11 changed files with 124 additions and 257 deletions
3
.idea/.gitignore
vendored
Normal file
3
.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
8
.idea/fftcgtool_v2.iml
Normal file
8
.idea/fftcgtool_v2.iml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
4
.idea/misc.xml
Normal file
4
.idea/misc.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Pipenv (fftcgtool_v2)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/fftcgtool_v2.iml" filepath="$PROJECT_DIR$/.idea/fftcgtool_v2.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
12
Pipfile
Normal file
12
Pipfile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
requests = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.6"
|
60
Pipfile.lock
generated
Normal file
60
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "8739d581819011fea34feca8cc077062d6bdfee39c7b37a8ed48c5e0a8b14837"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.6"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
|
||||||
|
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
|
||||||
|
],
|
||||||
|
"version": "==2021.5.30"
|
||||||
|
},
|
||||||
|
"charset-normalizer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
|
||||||
|
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3'",
|
||||||
|
"version": "==2.0.4"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||||
|
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3'",
|
||||||
|
"version": "==3.2"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
|
||||||
|
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.26.0"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
|
||||||
|
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||||
|
"version": "==1.26.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
89
card.py
89
card.py
|
@ -1,89 +0,0 @@
|
||||||
import requests
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
# Card front by Square API
|
|
||||||
FACEURL = "https://fftcg.cdn.sewest.net/images/cards/full/{}_eg.jpg"
|
|
||||||
|
|
||||||
# Card back image by Aurik
|
|
||||||
BACKURL = "http://cloud-3.steamusercontent.com/ugc/948455238665576576/85063172B8C340602E8D6C783A457122F53F7843/"
|
|
||||||
|
|
||||||
class Card:
|
|
||||||
def __init__(self, data):
|
|
||||||
# check if this is a card back
|
|
||||||
if data == 0:
|
|
||||||
self._serial = "0-000"
|
|
||||||
self._name = "[cardback]"
|
|
||||||
self._rarity = "X"
|
|
||||||
self._element = "None"
|
|
||||||
self._elements = None
|
|
||||||
|
|
||||||
self._description = "None"
|
|
||||||
self._iurl = BACKURL
|
|
||||||
|
|
||||||
# else import from data
|
|
||||||
else:
|
|
||||||
self._serial = data["serial_number"]
|
|
||||||
self._name = data["name"]
|
|
||||||
self._rarity = data["rarity"][0]
|
|
||||||
self._element = data["elements"][0]
|
|
||||||
|
|
||||||
# handle multi element cards
|
|
||||||
if len(data["elements"]) > 1:
|
|
||||||
self._element = "Multi"
|
|
||||||
self._elements = data["elements"]
|
|
||||||
|
|
||||||
# fix db typos
|
|
||||||
if self._serial == "12-052":
|
|
||||||
self._rarity = "H"
|
|
||||||
elif self._serial == "12-049":
|
|
||||||
self._rarity = "H"
|
|
||||||
|
|
||||||
self._description = "\n\n".join(data["abilities"])
|
|
||||||
self._iurl = FACEURL.format(self.get_id()) # official url
|
|
||||||
#self._iurl = data["image"] # ffdecks url
|
|
||||||
|
|
||||||
# 'Shinra' (Wind, 6-048C)
|
|
||||||
def __str__(self):
|
|
||||||
return "'{}' ({}, {})".format(self._name, self._element, self.get_id())
|
|
||||||
|
|
||||||
# 6-048C
|
|
||||||
def get_id(self):
|
|
||||||
rarity = self._rarity if self._rarity in ["C", "R", "H", "L", "S"] else ""
|
|
||||||
return "{}{}".format(self._serial, rarity)
|
|
||||||
|
|
||||||
# return in dictionary format
|
|
||||||
def get_dict(self):
|
|
||||||
return {
|
|
||||||
"Nickname": self._name,
|
|
||||||
"Description": self._description,
|
|
||||||
|
|
||||||
"Name": "Card",
|
|
||||||
"Transform": {
|
|
||||||
"scaleX": 2.17822933,
|
|
||||||
"scaleY": 1.0,
|
|
||||||
"scaleZ": 2.17822933
|
|
||||||
},
|
|
||||||
"Locked": False,
|
|
||||||
"Grid": True,
|
|
||||||
"Snap": True,
|
|
||||||
"Autoraise": True,
|
|
||||||
"Sticky": True,
|
|
||||||
"Tooltip": True,
|
|
||||||
"GridProjection": False,
|
|
||||||
"SidewaysCard": False,
|
|
||||||
"Hands": True
|
|
||||||
}
|
|
||||||
|
|
||||||
# download and resize card image
|
|
||||||
def get_image(self, resolution):
|
|
||||||
try:
|
|
||||||
response = requests.get(self._iurl)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
im = Image.open(BytesIO(response.content))
|
|
||||||
im = im.convert("RGB")
|
|
||||||
im = im.resize(resolution, Image.BICUBIC)
|
|
||||||
return im
|
|
0
fftcg/__init__.py
Normal file
0
fftcg/__init__.py
Normal file
23
fftcg/card.py
Normal file
23
fftcg/card.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Card:
|
||||||
|
def __init__(self, data):
|
||||||
|
code_match = re.match(r'([0-9]+)-([0-9]+)([CRHLSB])', data["Code"])
|
||||||
|
if code_match:
|
||||||
|
self._opus, self._serial, self._rarity = code_match.groups()
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._opus = "PR"
|
||||||
|
self._serial = re.match(r'PR-([0-9]+)', data["Code"]).group(1)
|
||||||
|
self._rarity = "P"
|
||||||
|
|
||||||
|
self._name = data["Name_EN"]
|
||||||
|
self._element = data["Element"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"'{self._name}' ({self._element}, {self.get_id()})"
|
||||||
|
|
||||||
|
# 6-048C
|
||||||
|
def get_id(self):
|
||||||
|
return f"{self._opus}-{self._serial}{self._rarity}"
|
168
opus.py
168
opus.py
|
@ -1,168 +0,0 @@
|
||||||
import queue
|
|
||||||
import threading
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
|
|
||||||
from card import Card, BACKURL
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
|
|
||||||
# multithreaded card image loading
|
|
||||||
class imageLoader(threading.Thread):
|
|
||||||
def __init__(self, queue, composite, composite_lock, grid, resolution):
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self.__queue = queue
|
|
||||||
self.__composite = composite
|
|
||||||
self.__lock = composite_lock
|
|
||||||
self.__grid = grid
|
|
||||||
self.__resolution = resolution
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
# shorthands
|
|
||||||
r, c = self.__grid # rows and columns per sheet
|
|
||||||
w, h = self.__resolution # width, height per card
|
|
||||||
|
|
||||||
while not self.__queue.empty():
|
|
||||||
# take next card
|
|
||||||
i, card = self.__queue.get()
|
|
||||||
|
|
||||||
# fetch card image (retry on fail)
|
|
||||||
while True:
|
|
||||||
logger.info("get image for card {}".format(card))
|
|
||||||
im = card.get_image(self.__resolution)
|
|
||||||
if im:
|
|
||||||
break
|
|
||||||
|
|
||||||
# paste image in correct position
|
|
||||||
self.__lock.acquire()
|
|
||||||
x, y = (i % c) * w, (i // c) * h
|
|
||||||
logger.info("paste image {} at P{}({}, {})".format(im.mode, i, x, y))
|
|
||||||
self.__composite.paste(im, (x, y, x+w, y+h))
|
|
||||||
self.__lock.release()
|
|
||||||
|
|
||||||
# image is processed
|
|
||||||
self.__queue.task_done()
|
|
||||||
|
|
||||||
|
|
||||||
class Opus:
|
|
||||||
def __init__(self, data):
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
self._cards = []
|
|
||||||
|
|
||||||
for card_data in data:
|
|
||||||
card = Card(card_data)
|
|
||||||
logger.info("Imported card {}".format(card))
|
|
||||||
self._cards.append(card)
|
|
||||||
|
|
||||||
# sort every element alphabetically
|
|
||||||
self._cards.sort(key=lambda x: x._serial)
|
|
||||||
self._cards.sort(key=lambda x: x._name)
|
|
||||||
self._cards.sort(key=lambda x: x._element)
|
|
||||||
|
|
||||||
def __get_sheets(self, grid):
|
|
||||||
# cards per sheet
|
|
||||||
count = grid[0]*grid[1] - 1
|
|
||||||
# flat copy
|
|
||||||
cards = self._cards
|
|
||||||
|
|
||||||
# while there are cards
|
|
||||||
while cards:
|
|
||||||
# get a chunk
|
|
||||||
yield cards[:count]
|
|
||||||
# remove that chunk
|
|
||||||
cards = cards[count:]
|
|
||||||
|
|
||||||
def get_images(self, grid, resolution, threadnum=16):
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
# shorthands
|
|
||||||
r, c = grid # rows and columns per sheet
|
|
||||||
w, h = resolution # width, height per card
|
|
||||||
|
|
||||||
for sheet in self.__get_sheets(grid):
|
|
||||||
# enqueue a sheet of cards
|
|
||||||
cardQueue = queue.Queue()
|
|
||||||
for i, card in enumerate(sheet):
|
|
||||||
cardQueue.put((i, card))
|
|
||||||
|
|
||||||
# add hidden face
|
|
||||||
hidden = Card(0)
|
|
||||||
cardQueue.put((r * c - 1, hidden))
|
|
||||||
|
|
||||||
# create a new card sheet
|
|
||||||
sheet = Image.new("RGB", (c*w, r*h))
|
|
||||||
logger.info("New image: {}x{}".format(*sheet.size))
|
|
||||||
|
|
||||||
# beware concurrent paste
|
|
||||||
sheet_lock = threading.Lock()
|
|
||||||
|
|
||||||
# start multithreading, wait for finish
|
|
||||||
for _ in range(threadnum):
|
|
||||||
imageLoader(cardQueue, sheet, sheet_lock, grid, resolution).start()
|
|
||||||
cardQueue.join()
|
|
||||||
|
|
||||||
# sheet image is generated, return now
|
|
||||||
yield sheet
|
|
||||||
|
|
||||||
def get_json(self, opusid, deckname, grid, cardfilter, faceurls):
|
|
||||||
# shorthands
|
|
||||||
r, c = grid # rows and columns per sheet
|
|
||||||
|
|
||||||
jsondict = { "ObjectStates": [ {
|
|
||||||
"Name": "Deck",
|
|
||||||
"Nickname": "Opus {} {}".format(opusid, deckname),
|
|
||||||
"Description": "",
|
|
||||||
|
|
||||||
"Transform": {
|
|
||||||
"scaleX": 2.17822933,
|
|
||||||
"scaleY": 1.0,
|
|
||||||
"scaleZ": 2.17822933
|
|
||||||
},
|
|
||||||
|
|
||||||
"Locked": False,
|
|
||||||
"Grid": True,
|
|
||||||
"Snap": True,
|
|
||||||
"Autoraise": True,
|
|
||||||
"Sticky": True,
|
|
||||||
"Tooltip": True,
|
|
||||||
"GridProjection": False,
|
|
||||||
"Hands": False,
|
|
||||||
"SidewaysCard": False,
|
|
||||||
|
|
||||||
"DeckIDs": [],
|
|
||||||
"CustomDeck": {},
|
|
||||||
"ContainedObjects": []
|
|
||||||
} ] }
|
|
||||||
|
|
||||||
for sheetnum, sheet in enumerate(self.__get_sheets(grid)):
|
|
||||||
# get current face
|
|
||||||
faceurl = faceurls[sheetnum]
|
|
||||||
# first sheet is "CustomDeck 1"
|
|
||||||
sheetnum = sheetnum + 1
|
|
||||||
|
|
||||||
# recurring sheet dictionary
|
|
||||||
sheetdict = { str(sheetnum): {
|
|
||||||
"FaceURL": faceurl,
|
|
||||||
"BackURL": BACKURL,
|
|
||||||
"NumWidth": c,
|
|
||||||
"NumHeight": r
|
|
||||||
} }
|
|
||||||
|
|
||||||
for cardnum, card in enumerate(sheet):
|
|
||||||
if not cardfilter(card):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# cardid 123 is on "CustomDeck 1", 3rd row, 4th column
|
|
||||||
cardid = sheetnum * 100 + (cardnum // c) * 10 + (cardnum % c) * 1
|
|
||||||
jsondict["ObjectStates"][0]["DeckIDs"].append(cardid)
|
|
||||||
|
|
||||||
# Add card object to ContainedObjects
|
|
||||||
carddict = card.get_dict()
|
|
||||||
carddict["CardID"] = cardid
|
|
||||||
carddict["CustomDeck"] = sheetdict
|
|
||||||
jsondict["ObjectStates"][0]["ContainedObjects"].append(carddict)
|
|
||||||
|
|
||||||
# Add sheet to CustomDecks
|
|
||||||
jsondict["ObjectStates"][0]["CustomDeck"].update(sheetdict)
|
|
||||||
|
|
||||||
return json.dumps(jsondict, indent=2)
|
|
Loading…
Reference in a new issue