Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 77 additions & 191 deletions LWCProto.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
#! /usr/bin/env python3
import csv
import os
import argparse
import re
import numpy as np
from pathlib import Path

import cairo
import argparse


import layout
from draw_card import drawCard
Expand All @@ -16,211 +13,100 @@


def extant_file(x):
"""
'Type' for argparse - checks that file exists but does not open.
"""
if not os.path.exists(x):
# Argparse uses the ArgumentTypeError to give a rejection message like:
# error: argument input: x does not exist
raise argparse.ArgumentTypeError("{0} does not exist".format(x))
return x
"""Argparse helper that ensures a file exists without opening it."""
path = Path(x)
if not path.exists():
raise argparse.ArgumentTypeError(f"{x} does not exist")
return str(path)



##### CLI args #####
parser = argparse.ArgumentParser(description="Deck Generator for Game Designers")
parser.add_argument('-d', '--deck', type=extant_file, help='csv file containing the deck', metavar="FILE")
parser.add_argument('-c', '--cards', type=extant_file, help='json file containing cards description', metavar="FILE", required=True)

parser = argparse.ArgumentParser(description="Card generator for game prototypes")
parser.add_argument(
'-c',
'--cards',
type=extant_file,
help='json file containing cards description',
metavar='FILE',
required=True,
)
parser.add_argument('-i', '--images', help='Add images to cards', action='store_true')
parser.add_argument('-r', '--rgb', help='Update layout card border colour with given R,G,B, only works with default layout', nargs=3, type=int)
parser.add_argument('-l', '--layout', help='Use a different layout than default', type=extant_file, metavar="FILE")
parser.add_argument('--single-card', help='Render each card as an individual 63x85mm PNG at 300 DPI', action='store_true')


args = parser.parse_args()

handle_images = args.images
modify_layout = args.rgb
cards_file = args.cards
single_card_mode = args.single_card
deck_file = args.deck

if single_card_mode and deck_file is not None:
parser.error('the --single-card option cannot be used together with --deck/-d')

if (not single_card_mode) and deck_file is None:
parser.error('the --deck/-d option is required unless --single-card is specified')

cards = CardDeck(cards_file)

nameList = []
list_copy = []
cardList = []
for entry in cards.getDb().values():
card = CardModel()
card.load(entry)
cardList.append(card)

if single_card_mode:
deck_name = os.path.splitext(os.path.basename(cards_file))[0]
cardList = []
for entry in cards.getDb().values():
card = CardModel()
card.load(entry)
cardList.append(card)
else:
deck_name = os.path.basename(deck_file)[:-4]
with open(deck_file, encoding='utf-8') as csvFile:
reader = csv.reader(csvFile)
list_copy.append(reader.__next__())
for row in reader:
list_copy.append(row)
nameList = nameList + [row[1]] * int(row[0])

cardList = [CardModel(name, cards.getDb()) for name in nameList]
pageList = [cardList[i:i+9] for i in range(0, len(cardList), 9)]

if handle_images or (modify_layout is not None):
from add_images import BaseImage
output_root = Path('output') / Path(cards_file).stem
cards_output_dir = output_root / 'cards'
cards_output_dir.mkdir(parents=True, exist_ok=True)

if handle_images:
from add_images import BaseImage
from add_images import addImage
from add_images import processImage
from add_images import load_full_frame_surface

if not os.path.exists('decks'):
os.mkdir('decks')
if not os.path.exists(os.path.join('decks',deck_name)):
os.mkdir(os.path.join('decks',deck_name))

if single_card_mode:
cards_output_dir = os.path.join('decks', deck_name, 'cards')
os.makedirs(cards_output_dir, exist_ok=True)
single_dpi = layout.SINGLE_CARD_DPI

def _slugify(value: str) -> str:
value = value.strip()
value = re.sub(r'\s+', '_', value)
value = re.sub(r'[^A-Za-z0-9_-]', '', value)
return value or 'card'

for index, card in enumerate(cardList):
print(f'Card {index}: {card}')
surf = layout.get_single_card_surface(single_dpi)
ctx = cairo.Context(surf)

card_matrix = layout.get_single_card_matrix(single_dpi)
ctx.set_matrix(card_matrix)
layout.clip_card(ctx)
ctx.set_source_rgb(1, 1, 1)
ctx.paint()


if handle_images and card.imageFullFrame:
full_frame_surface = load_full_frame_surface(card, single_dpi)
if full_frame_surface is not None:
ctx.save()
ctx.identity_matrix()
ctx.set_source_surface(full_frame_surface, 0, 0)
ctx.paint()
ctx.restore()

ctx.reset_clip()
ctx.set_matrix(card_matrix)
drawCard(card, ctx)
card_filename = f"{index:03d}_{_slugify(card.headerText)}.png"
output_path = os.path.join(cards_output_dir, card_filename)
surf.write_to_png(output_path)

if handle_images and not card.imageFullFrame:
processImage(card, deck_name, dpi=single_dpi)
baseImage = BaseImage(output_path)
updated_image = addImage(card, baseImage, deck_name, dpi=single_dpi)
baseImage.update(updated_image)
baseImage.save(output_path)
image_output_dir = output_root / 'images'
else:
for page_number in range(len(pageList)):
print(f'Page {page_number}:')
page = pageList[page_number]
surf = layout.getSurface()
ctx = cairo.Context(surf)

page_dpi = layout.get_surface_dpi(surf)

for i in range(len(page)):
card = page[i]
cardPos = (i % 3, i // 3)
print(cardPos)
print(card)

if handle_images and card.imageFullFrame:
full_frame_surface = load_full_frame_surface(card, page_dpi)
if full_frame_surface is not None:
card_origin_mm = layout.get_card_origin_mm(cardPos)
origin_px = layout.pair_mm_to_pixels(
card_origin_mm,
page_dpi,
)
ctx.save()
ctx.identity_matrix()
layout.clip_card_absolute(ctx, card_origin_mm, page_dpi)
ctx.set_source_surface(full_frame_surface, *origin_px)
ctx.paint()
ctx.restore()

mat = layout.getMatrix(*cardPos, surf)
ctx.set_matrix(mat)
drawCard(card, ctx)

output_path = f'decks/{deck_name}/{deck_name}_p{page_number}.png'
surf.write_to_png(output_path)

if (modify_layout is not None):
from PIL import Image

baseImage = BaseImage(output_path)
temp = baseImage.baseImage.convert('RGBA')
data = np.array(temp)
red, green, blue, alpha = data.T
for i in range(0,63):
white_areas = (red == 190+i) & (blue == 190+i) & (green == 190+i)
data[..., :-1][white_areas.T] = (modify_layout[0], modify_layout[1], modify_layout[2])
baseImage.update(Image.fromarray(data))
baseImage.save(output_path)


#import pdb;pdb.set_trace()
if handle_images:
page_dpi = layout.get_surface_dpi(surf)
needs_partial_images = any(
card.image is not None and not card.imageFullFrame for card in page
)

if needs_partial_images:
baseImage = BaseImage(output_path)
for i in range(len(page)):
card = page[i]
if card.image is None or card.imageFullFrame:
continue

cardPos = (i % 3, i // 3)
card_origin_mm = layout.get_card_origin_mm(cardPos)
image_position_mm = (
card_origin_mm[0] + layout.ART_OFFSET_MM[0],
card_origin_mm[1] + layout.ART_OFFSET_MM[1],
)
processImage(card, deck_name, dpi=page_dpi)
baseImage.update(
addImage(
card,
baseImage,
deck_name,
position_mm=image_position_mm,
dpi=page_dpi,
)
)

baseImage.save(output_path)


if not single_card_mode:
with open(f'decks/{deck_name}/{deck_name}.csv', 'w') as deck_copy:
filewriter = csv.writer(deck_copy)
for element in list_copy:
filewriter.writerow(element)
image_output_dir = None

single_dpi = layout.SINGLE_CARD_DPI


def _slugify(value: str) -> str:
value = value.strip()
value = re.sub(r'\s+', '_', value)
value = re.sub(r'[^A-Za-z0-9_-]', '', value)
return value or 'card'


for index, card in enumerate(cardList):
print(f'Card {index}: {card}')
surf = layout.get_single_card_surface(single_dpi)
ctx = cairo.Context(surf)

card_matrix = layout.get_single_card_matrix(single_dpi)
ctx.set_matrix(card_matrix)
layout.clip_card(ctx)
ctx.set_source_rgb(1, 1, 1)
ctx.paint()

if handle_images and card.imageFullFrame:
full_frame_surface = load_full_frame_surface(card, single_dpi)
if full_frame_surface is not None:
ctx.save()
ctx.identity_matrix()
ctx.set_source_surface(full_frame_surface, 0, 0)
ctx.paint()
ctx.restore()

ctx.reset_clip()
ctx.set_matrix(card_matrix)
drawCard(card, ctx)

card_filename = f"{index:03d}_{_slugify(card.headerText)}.png"
output_path = cards_output_dir / card_filename
surf.write_to_png(str(output_path))

if handle_images and not card.imageFullFrame:
processImage(card, output_dir=image_output_dir, dpi=single_dpi)
baseImage = BaseImage(str(output_path))
updated_image = addImage(
card,
baseImage,
output_dir=image_output_dir,
dpi=single_dpi,
)
baseImage.update(updated_image)
baseImage.save(str(output_path))
25 changes: 5 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,14 @@ En principio todo lo que se necesita para ejecutar este script es python 3.9 ins
### Sintaxis

```
usage: LWCProto.py [-h] -d FILE -c FILE [-i] [-r RGB RGB RGB] [-l FILE]
usage: LWCProto.py [-h] -c FILE [-i]

Deck Generator for Game Designers
Card generator for game prototypes

optional arguments:
options:
-h, --help show this help message and exit
-d FILE, --deck FILE csv file containing the deck
-c FILE, --cards FILE
json file containing cards description
-c FILE, --cards FILE json file containing cards description
-i, --images Add images to cards
-r RGB RGB RGB, --rgb RGB RGB RGB
Update layout card border colour with given R,G,B, only works with default layout
-l FILE, --layout FILE (==> Not ready yet)
Use a different layout than default
```
### Archivo de definicion de cartas:

Expand Down Expand Up @@ -71,16 +65,7 @@ El archivo de definicion de cartas es un archivo en jormato json con el siguente
```
El objeto `header` define el texto visible en la parte superior de la carta. El campo `color` ajusta el color del texto, mientras que los campos `banner` y `banner_color` permiten activar un recuadro de color sólido detrás del encabezado cuando sea necesario.
El bloque `card_text` permite especificar el texto del cuerpo y el color con el que debe renderizarse. Para las imágenes puedes indicar un nombre de archivo directamente o un objeto con las claves `source` y `full_frame`. Cuando `full_frame_image` (o `full_frame` en el objeto de imagen) es `true`, la ilustración se ampliará para cubrir toda la carta; en caso contrario se mantendrá dentro del marco de arte.
Las imagenes deben almacenarse en el directorio "images" que se encuentra en la misma carpeta que LWCProto.py, el formato de las imagenes es indiferente y su tamaño tambien estas seran redimensionadas automaticamente para adaptarse al tamaño disponible en el layout

### Archivo de definicion del mazo

Archivo en formato csv que tiene el siguiente formato:
```
Qty,Name
40,CartID_1
300,CartID_2
```
Las imagenes deben almacenarse en el directorio "images" que se encuentra en la misma carpeta que LWCProto.py, el formato de las imagenes es indiferente y su tamaño tambien estas seran redimensionadas automaticamente para adaptarse al tamaño disponible en el layout. El script genera los resultados dentro del directorio `output/<nombre_del_archivo>/cards`, creando versiones individuales en PNG de cada carta del archivo JSON. Si se habilitan las imágenes (`--images`), también se creará una carpeta `output/<nombre_del_archivo>/images` con las versiones redimensionadas de las ilustraciones.
## Fuentes

Este proyecto se basa en el original [LightWeithgMTGProxy](https://github.com/tilleraj/LightWeightMTGProxy) todo el credito le pertenece a él, hacemos extensivo sus agradecimientos a .Rai de Cardgame Coalition creador del layout original que estamos utilizando
12 changes: 6 additions & 6 deletions add_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
except AttributeError:
_RESAMPLE = Image.LANCZOS

def _ensure_output_dir(deck: str) -> pathlib.Path:
path = pathlib.Path('decks') / deck / 'images'
def _ensure_output_dir(path: pathlib.Path) -> pathlib.Path:
path = pathlib.Path(path)
path.mkdir(parents=True, exist_ok=True)
return path

Expand Down Expand Up @@ -51,16 +51,16 @@ def _load_resized_source_image(image_name: str, size_px):

def processImage(
card: card_model.CardModel,
deck: str,
*,
output_dir: pathlib.Path,
size_mm=layout.ART_SIZE_MM,
dpi: int = layout.SINGLE_CARD_DPI,
):
if card.image is None:
return

size_px = layout.pair_mm_to_pixels(size_mm, dpi)
output_dir = _ensure_output_dir(deck)
output_dir = _ensure_output_dir(output_dir)
destination = output_dir / str(card.image)

if destination.exists():
Expand All @@ -82,8 +82,8 @@ def processImage(
def addImage(
card: card_model.CardModel,
base: BaseImage,
deck: str,
*,
output_dir: pathlib.Path,
position_mm=None,
size_mm=layout.ART_SIZE_MM,
dpi: int = layout.SINGLE_CARD_DPI,
Expand All @@ -92,7 +92,7 @@ def addImage(
if card.image is None:
return base.get()

output_dir = _ensure_output_dir(deck)
output_dir = _ensure_output_dir(output_dir)
image_path = output_dir / str(card.image)

try:
Expand Down
Loading
Loading