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 0000000..787d2ac Binary files /dev/null and b/examples/bard1.jpeg differ 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