diff --git a/LWCProto.py b/LWCProto.py index d914da1..fb8ae6a 100644 --- a/LWCProto.py +++ b/LWCProto.py @@ -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 @@ -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)) diff --git a/README.md b/README.md index b16c238..5a82d1b 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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//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//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 diff --git a/add_images.py b/add_images.py index 34f9d77..c981bb6 100644 --- a/add_images.py +++ b/add_images.py @@ -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 @@ -51,8 +51,8 @@ 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, ): @@ -60,7 +60,7 @@ def processImage( 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(): @@ -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, @@ -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: diff --git a/tests/test_card_model.py b/tests/test_card_model.py index b502974..20a800e 100644 --- a/tests/test_card_model.py +++ b/tests/test_card_model.py @@ -106,19 +106,6 @@ def test_load_supports_legacy_name_field(self): self.assertEqual(card.headerColour, "#000000") self.assertEqual(card.typeStr, "Enchantment") - def test_load_supports_legacy_name_field(self): - data = { - "name": "Legacy", - "type": "Enchantment", - } - - card = CardModel() - card.load(data) - - self.assertEqual(card.headerText, "Legacy") - self.assertEqual(card.headerColour, "#000000") - self.assertEqual(card.typeStr, "Enchantment") - class CardDeckLoadTest(unittest.TestCase): def test_load_uses_provided_path(self):