From 5183a6301d9aa12fcc60a0cfb611fe8b699d64fb Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Wed, 21 Jul 2021 17:18:31 -0500 Subject: [PATCH] Added character portraits from @vincentmalloy. Merge https://github.com/canismarko/dungeon-sheets/pull/100 --- docs/character_files.rst | 14 +++++++ dungeonsheets/character.py | 2 +- dungeonsheets/create_character.py | 0 dungeonsheets/fill_pdf_template.py | 59 ++++++++++++++++++++++++----- dungeonsheets/make_sheets.py | 6 ++- examples/bard1.jpeg | Bin 0 -> 24880 bytes examples/bard1.py | 2 + requirements.txt | 1 + setup.py | 2 +- tests/test_character.py | 0 10 files changed, 73 insertions(+), 13 deletions(-) mode change 100755 => 100644 dungeonsheets/create_character.py mode change 100755 => 100644 dungeonsheets/make_sheets.py create mode 100644 examples/bard1.jpeg mode change 100755 => 100644 tests/test_character.py diff --git a/docs/character_files.rst b/docs/character_files.rst index f5120f0..a5c3169 100644 --- a/docs/character_files.rst +++ b/docs/character_files.rst @@ -51,6 +51,20 @@ standard 5e rules, and are case-insensitive. Refer to the D&D xp = 2190 hp_max = 16 +Character Portrait +================== + +.. code:: python + + portrait = True + +If this is set to True and a corresponding portrait file exists, +the portrait will be added to the character personality sheet. +For now, the file must have a .jpeg extension and be named exactly +the same as the character file. This might not work with every Image size. +ca 550 * 700px seems to be the right format. Anything smaller should work, too. +See the Bard1 example for a demonstration of this feature. + Ability Scores ============== diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 39f8cad..c364b05 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -97,7 +97,7 @@ class Character(Entity): _proficiencies_text = list() # Appearance - # portrait = placeholder not sure how to implement + portrait = False age = 0 height = "" weight = "" diff --git a/dungeonsheets/create_character.py b/dungeonsheets/create_character.py old mode 100755 new mode 100644 diff --git a/dungeonsheets/fill_pdf_template.py b/dungeonsheets/fill_pdf_template.py index 1111d0e..d52f3f8 100644 --- a/dungeonsheets/fill_pdf_template.py +++ b/dungeonsheets/fill_pdf_template.py @@ -2,9 +2,11 @@ import os import subprocess import logging import warnings +import io import pdfrw from fdfgen import forge_fdf +from reportlab.pdfgen import canvas from dungeonsheets.forms import mod_str @@ -175,10 +177,10 @@ def create_character_pdf_template(character, basename, flatten=False): # Prepare the actual PDF dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "forms/") src_pdf = os.path.join(dirname, "blank-character-sheet-default.pdf") - return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) + return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten, portrait="") -def create_personality_pdf_template(character, basename, flatten=False): +def create_personality_pdf_template(character, basename, portrait_file="", flatten=False): # Prepare the list of fields fields = { "CharacterName 2": character.name, @@ -199,7 +201,7 @@ def create_personality_pdf_template(character, basename, flatten=False): # Prepare the actual PDF dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "forms/") src_pdf = os.path.join(dirname, "blank-personality-sheet-default.pdf") - return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) + return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten, portrait=portrait_file) def create_spells_pdf_template(character, basename, flatten=False): @@ -484,10 +486,10 @@ def create_spells_pdf_template(character, basename, flatten=False): # Make the actual pdf dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "forms/") src_pdf = os.path.join(dirname, "blank-spell-sheet-default.pdf") - make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) + make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten, portrait="") -def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False): +def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False, portrait = ""): """Create a new PDF by applying fields to a src PDF document. Parameters @@ -506,17 +508,17 @@ def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False): """ try: - _make_pdf_pdftk(fields, src_pdf, basename, flatten) + _make_pdf_pdftk(fields, src_pdf, basename, flatten, portrait) except FileNotFoundError: # pdftk could not run, so alert the user and use pdfrw warnings.warn( f"Could not run `{PDFTK_CMD}`, using fallback; forcing `--editable`.", RuntimeWarning, ) - _make_pdf_pdfrw(fields, src_pdf, basename, flatten) + _make_pdf_pdfrw(fields, src_pdf, basename, flatten, portrait) -def _make_pdf_pdfrw(fields: dict, src_pdf: str, basename: str, flatten: bool = False): +def _make_pdf_pdfrw(fields: dict, src_pdf: str, basename: str, flatten: bool = False, portrait = ""): """Backup make_pdf function in case pdftk is not available.""" template = pdfrw.PdfReader(src_pdf) # Different types of PDF fields @@ -575,7 +577,7 @@ def _make_pdf_pdfrw(fields: dict, src_pdf: str, basename: str, flatten: bool = F pdfrw.PdfWriter().write(f"{basename}.pdf", template) -def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False): +def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False, portrait=""): """More robust way to make a PDF, but has a hard dependency.""" # Create the actual FDF file fdfname = basename + ".fdf" @@ -585,7 +587,12 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False): fdf_file.write(fdf) fdf_file.close() # Build the final flattened PDF documents - dest_pdf = basename + ".pdf" + if portrait != "": + dest_pdf = basename + "-temp.pdf" + image_pdf = basename + "_image_tmp.pdf" + make_image_pdf(portrait, image_pdf) + else: + dest_pdf = basename + ".pdf" popenargs = [ PDFTK_CMD, src_pdf, @@ -597,5 +604,37 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False): if flatten: popenargs.append("flatten") subprocess.call(popenargs) + # stamp with image + if portrait != "": + src_pdf = dest_pdf + stamped_pdf = basename + ".pdf" + popenargs = [ + PDFTK_CMD, + src_pdf, + "stamp", + image_pdf, + "output", + stamped_pdf, + ] + popenargs.append("flatten") + subprocess.call(popenargs) + # Clean up + os.remove(image_pdf) + os.remove(dest_pdf) # Clean up temporary files os.remove(fdfname) + +def make_image_pdf(src_img:str, dest_pdf:str): + packet = io.BytesIO() + can = canvas.Canvas(packet) + x_start = 10 + y_start = 240 + can.drawImage(src_img, x_start, y_start, width=175, preserveAspectRatio=True, mask='auto') + can.showPage() + can.save() + + #move to the beginning of the StringIO buffer + packet.seek(0) + + new_pdf = pdfrw.PdfReader(packet) + pdfrw.PdfWriter().write(dest_pdf, new_pdf) diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py old mode 100755 new mode 100644 index 235d62f..804eba1 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -420,6 +420,10 @@ def make_character_sheet( if character is None: character_props = readers.read_sheet_file(char_file) character = _char.Character.load(character_props) + # Load image file if present + portrait_file="" + if character.portrait: + portrait_file=char_file.stem + ".jpeg" # Set the fields in the FDF basename = char_file.stem char_base = basename + "_char" @@ -440,7 +444,7 @@ def make_character_sheet( ) pages.append(char_pdf) person_pdf = create_personality_pdf_template( - character=character, basename=person_base, flatten=flatten + character=character, basename=person_base, portrait_file=portrait_file, flatten=flatten ) pages.append(person_pdf) if character.is_spellcaster: diff --git a/examples/bard1.jpeg b/examples/bard1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..787d2aca37b15e59ecdd432a3f97923fe1c7d838 GIT binary patch literal 24880 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 SH6+xKP(#B1B?+xQPy7Xu+PQN8 literal 0 HcmV?d00001 diff --git a/examples/bard1.py b/examples/bard1.py index 94d8a01..f749f06 100644 --- a/examples/bard1.py +++ b/examples/bard1.py @@ -107,3 +107,5 @@ flaws = """TODO: Describe your characters interesting flaws. features_and_traits = """TODO: Describe other features and abilities your character has.""" + +portrait = True diff --git a/requirements.txt b/requirements.txt index 49e78fb..3d26667 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ jinja2 sphinx pdfrw EbookLib +reportlab diff --git a/setup.py b/setup.py index c1b9f76..50112cd 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup(name='dungeonsheets', }, install_requires=[ 'fdfgen', 'npyscreen', 'jinja2', 'pdfrw', 'sphinx', - 'EbookLib', + 'EbookLib', 'reportlab', ], entry_points={ 'console_scripts': [ diff --git a/tests/test_character.py b/tests/test_character.py old mode 100755 new mode 100644