From 811de70341cc30ab3cf3658eaa94d01e7661073d Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Wed, 10 Apr 2024 17:09:19 -0700 Subject: [PATCH] Improve logging; simplify font handling (#510) --- pyproject.toml | 2 +- src/domdiv/cards.py | 13 +---- src/domdiv/draw.py | 99 +++++++++++++++------------------ src/domdiv/fonts/README.md | 14 ++--- src/domdiv/main.py | 110 ++++++++++++++++--------------------- 5 files changed, 99 insertions(+), 139 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8199dc6c..5cd6e009 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "domdiv" dynamic = ["version"] -dependencies = ["reportlab", "Pillow", "configargparse"] +dependencies = ["reportlab", "Pillow", "configargparse", "loguru"] description = "Divider Generation for the Dominion Card Game" keywords = ["boardgame", "cardgame", "dividers"] authors = [{ name = "Peter Gorniak", email = "sumpfork@mailmight.net" }] diff --git a/src/domdiv/cards.py b/src/domdiv/cards.py index bdb0c4c8..82bd403b 100644 --- a/src/domdiv/cards.py +++ b/src/domdiv/cards.py @@ -1,6 +1,7 @@ import json import re +from loguru import logger from reportlab.lib.units import cm @@ -208,11 +209,7 @@ def setImage(self, use_set_icon=False): setImage = Card.sets[self.cardset_tag]["image"] if setImage is None and self.cardset_tag != "base": - print( - 'warning, no set image for set "{}", card "{}"'.format( - self.cardset, self.name - ) - ) + logger.warning(f'no set image for set "{self.cardset}", card "{self.name}"') return setImage def setTextIcon(self): @@ -225,11 +222,7 @@ def setTextIcon(self): setTextIcon = Card.sets[self.cardset_tag]["text_icon"] if setTextIcon is None and self.cardset != "base": - print( - 'warning, no set text for set "{}", card "{}"'.format( - self.cardset, self.name - ) - ) + logger.warning(f'no set text for set "{self.cardset}", card "{self.name}"') return setTextIcon def isBlank(self): diff --git a/src/domdiv/draw.py b/src/domdiv/draw.py index dfe7090b..e9e63929 100644 --- a/src/domdiv/draw.py +++ b/src/domdiv/draw.py @@ -2,9 +2,9 @@ import numbers import os import re -import sys import pkg_resources +from loguru import logger from PIL import Image, ImageEnhance from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT from reportlab.lib.styles import getSampleStyleSheet @@ -586,6 +586,7 @@ def registerFonts(self): if pkg_resources.resource_exists("domdiv", fpath): fontpaths[font] = (fpath, False) break + logger.trace(fontpaths) # Mark the built-in files as pre-registered registered = { font: None for font, fontpath in fontpaths.items() if fontpath is None @@ -593,37 +594,10 @@ def registerFonts(self): # Check if *all three* Times Roman TTF fonts have been found and register them. If not -> remove all three # from the paths list - timesTTFfound = ( - "Times-Roman-TTF" in fontpaths - and "Times-Bold-TTF" in fontpaths - and "Times-Italic-TTF" in fontpaths - ) - if not langlatin1 and timesTTFfound: - pdfmetrics.registerFont( - TTFont( - "Times-Roman-TTF", - pkg_resources.resource_filename( - "domdiv", fontpaths.get("Times-Roman-TTF")[0] - ), - ) - ) - pdfmetrics.registerFont( - TTFont( - "Times-Bold-TTF", - pkg_resources.resource_filename( - "domdiv", fontpaths.get("Times-Bold-TTF")[0] - ), - ) - ) - pdfmetrics.registerFont( - TTFont( - "Times-Italic-TTF", - pkg_resources.resource_filename( - "domdiv", fontpaths.get("Times-Italic-TTF")[0] - ), - ) - ) + timesTTFs = set(["Times-Roman-TTF", "Times-Bold-TTF", "Times-Italic-TTF"]) + timesTTF_not_found = timesTTFs - set(fontpaths) + if not langlatin1 and not timesTTF_not_found: # Register Times Roman TTF as font family. Necessary for and attributes to work in Platypus! pdfmetrics.registerFontFamily( "Times-Roman-TTF", @@ -631,55 +605,71 @@ def registerFonts(self): bold="Times-Bold-TTF", italic="Times-Italic-TTF", ) - - # Since we registered them already, mark the Times Roman TTF as pre-registered - registered.update({"Times-Roman-TTF": None}) - registered.update({"Times-Bold-TTF": None}) - registered.update({"Times-Italic-TTF": None}) else: - fontpaths.pop("Times-Roman-TTF", None) - fontpaths.pop("Times-Bold-TTF", None) - fontpaths.pop("Times-Italic-TTF", None) + if not langlatin1: + logger.warning( + f"Non-Latin1 language requested ({self.options.language}) " + "but Times TTF font files not provided (black boxes may appear). " + f"missing fonts: {timesTTF_not_found}" + ) + for fontname in timesTTFs: + fontpaths.pop(fontname, None) # Determine the best matching fonts for each font type. fontprefs = { "Name": [ # card names & types "TrajanPro-Bold", "MinionPro-Regular", - "Times-Roman" if langlatin1 or not timesTTFfound else "Times-Roman-TTF", + ( + "Times-Roman" + if langlatin1 or timesTTF_not_found + else "Times-Roman-TTF" + ), ], "Expansion": [ # expansion names "CharlemagneStd-Bold", "TrajanPro-Bold", "MinionPro-Regular", - "Times-Roman" if langlatin1 or not timesTTFfound else "Times-Roman-TTF", + ( + "Times-Roman" + if langlatin1 or timesTTF_not_found + else "Times-Roman-TTF" + ), ], "Cost": [ # card costs (coins, debt, etc) "MinionStd-Black", "MinionPro-Bold", - "Times-Bold" if langlatin1 or not timesTTFfound else "Times-Bold-TTF", + "Times-Bold" if langlatin1 or timesTTF_not_found else "Times-Bold-TTF", ], "PlusCost": [ # card cost superscript "+" modifiers "Helvetica-Bold", ], "Regular": [ # regular text "MinionPro-Regular", - "Times-Roman" if langlatin1 or not timesTTFfound else "Times-Roman-TTF", + ( + "Times-Roman" + if langlatin1 or timesTTF_not_found + else "Times-Roman-TTF" + ), ], "Bold": [ # miscellaneous bold text "MinionPro-Bold", - "Times-Bold" if langlatin1 or not timesTTFfound else "Times-Bold-TTF", + "Times-Bold" if langlatin1 or timesTTF_not_found else "Times-Bold-TTF", ], "Italic": [ # for --use-set-text-icon "MinionPro-Italic", ( "Times-Italic" - if langlatin1 or not timesTTFfound + if langlatin1 or timesTTF_not_found else "Times-Italic-TTF" ), ], "Rules": [ - "Times-Roman" if langlatin1 or not timesTTFfound else "Times-Roman-TTF", + ( + "Times-Roman" + if langlatin1 or timesTTF_not_found + else "Times-Roman-TTF" + ), ], "Monospaced": [ "Courier", @@ -693,15 +683,13 @@ def registerFonts(self): for style, font in self.fontStyle.items(): best = fontprefs[style][0] if font != best: - print( - "Warning, {} missing from font dirs; " - "using {} instead.".format(best, font), - file=sys.stderr, + logger.warning( + f"Font {best} missing from font dirs; using {font} instead." ) if font in registered: continue fontpath, is_local = fontpaths[font] - # print("Registering {} = {}".format(font, fontpath)) + logger.trace(f"Registering {font} = {fontpath}") pdfmetrics.registerFont( TTFont( font, @@ -1725,10 +1713,9 @@ def drawTab(self, item, panel=None, backside=False): else: name_lines = lname, (lmid + rmid + " " + rname).rstrip() else: # nowhere to break - print( - f"Could not break up long card name: {name}", - round(width / cm, 2), - round(textWidth / cm, 2), + logger.debug( + f"Could not break up long card name: {name} " + f"{round(width / cm, 2)} {round(textWidth / cm, 2)}", ) name_lines = (name,) else: @@ -2291,7 +2278,7 @@ def calculatePages(self, cards): if options.wrapper: # Adjust height for wrapper. Use the maximum thickness of any divider so we know anything will fit. maxStackHeight = max(c.getStackHeight(options.thickness) for c in cards) - print("Max Card Stack Height: {:.2f}cm ".format(maxStackHeight / cm)) + logger.info(f"Max Card Stack Height: {maxStackHeight / cm:.2f}cm ") options.dividerHeightReserved = totalHeight(options, maxStackHeight) # Adjust for rotation diff --git a/src/domdiv/fonts/README.md b/src/domdiv/fonts/README.md index 4f1dd73b..77115128 100644 --- a/src/domdiv/fonts/README.md +++ b/src/domdiv/fonts/README.md @@ -1,18 +1,12 @@ ## Fonts -I believe I cannot distribute one font (Minion Pro) domdiv uses as they are owned by Adobe with a License that I understand to disallow redistribution. However, you can download the [3 font files from a third party here](https://www.dropbox.com/s/tsqk69mayoa3pfz/MinionPro-ForDominionTabs.zip?dl=1). +I believe I cannot distribute some fonts (Minion Pro, Trajan, Charlgemagne) domdiv uses as they are owned by Adobe with a License that I understand to disallow redistribution. You can look for them on fontsgeek.com or similar sites. -Other sources for the font files (included for historical record but probably unneeded as long as long as the download above works: +Alternatively, many font files are included with every install of the free Adobe Reader. For example, on Windows 7 they are in `C:\Program Files (x86)\Adobe\Reader 9.0\Resource\Font`. -- http://fontsgeek.com/fonts/Minion-Pro-Regular -- http://fontsgeek.com/fonts/Minion-Pro-Italic -- http://fontsgeek.com/fonts/Minion-Pro-Bold +Sadly, all these fonts use features that are not support by the reportlab package. Thus, they need to first be converted to ttf (TrueType) format. I used the open source package fontforge to do the conversion. Included as 'tools/convert.ff' is a script for fontforge to do the conversion, on Mac OS X with fontforge installed through homebrew you can just run, for example, `./tools/convert.ff MinionPro-Regular.otf`. -Alternatively, the font files are included with every install of the free Adobe Reader. For example, on Windows 7 they are in C:\Program Files (x86)\Adobe\Reader 9.0\Resource\Font called `MinionPro-Regular.otf`, `MinionPro-Bold.otf` and `MinionPro-It.otf`. - -Sadly, all these fonts use features that are not support by the reportlab package. Thus, they need to first be converted to ttf (TrueType) format. I used the open source package fontforge to do the conversion. Included as 'convert.ff' is a script for fontforge to do the conversion, on Mac OS X with fontforge installed through macports or homebrew you can just run `./convert.ff MinionPro-Regular.otf`, `./convert.ff MinionPro-Bold.otf` and `./convert.ff MinionPro-It.otf`. With other fontforge installations, you'll need to change the first line of convert.ff to point to your fontforge executable. I have not done this step under Windows - I imagine it may be possible with a cygwin install of fontforge or some such method. - -Copy the converted `.ttf` files to the `fonts` directory in the `domdiv` package/directory, then perform the package install below. +Copy the converted `.ttf` files to the `fonts` directory in the `domdiv` package/directory, then perform the package install below. Or, you can put them into any directory and provide the `--font-dir` option to point to it when you call the script, even if it's installed from pypi. If you select language in `domdiv` options which is not supported in [ISO/IEC 8859-1:1998 (Latin1)](https://en.wikipedia.org/wiki/ISO/IEC_8859-1#Modern_languages_with_complete_coverage) (e.g. Czech), you will have to obtain Times Roman TTF fonts as well: diff --git a/src/domdiv/main.py b/src/domdiv/main.py index 6bf68137..421dc31e 100644 --- a/src/domdiv/main.py +++ b/src/domdiv/main.py @@ -11,6 +11,7 @@ import configargparse import pkg_resources import reportlab.lib.pagesizes as pagesizes +from loguru import logger from reportlab.lib.units import cm from .cards import Card, CardType @@ -22,10 +23,6 @@ have_icu = True except ImportError: have_icu = False - print( - "** Warning: PyICU library not found. The dividers will be ordered by default sort key (might not be the " - "correct alphabetical order for the selected language)." - ) LOCATION_CHOICES = ["tab", "body-top", "hide"] NAME_ALIGN_CHOICES = ["left", "right", "centre", "edge"] @@ -296,7 +293,7 @@ def parse_opts(cmdline_args=None): group_tab.add_argument( "--tab-number", type=int, - default=1, + default=2, help="The number of tabs. When set to 1, all tabs are on the same side (specified by --tab_side). " "When set to 2, tabs will alternate between left and right. (starting side specified by --tab_side). " "When set > 2, the first tab will be on left/right side specified by --tab_side, then the rest " @@ -949,7 +946,12 @@ def parse_opts(cmdline_args=None): is_write_out_config_file_arg=True, help="Write out the given options to the specified configuration file.", ) - + group_special.add_argument( + "--log-level", + default="WARNING", + help="Set the logging level.", + choices=["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + ) options = parser.parse_args(args=cmdline_args) # Need to do these while we have access to the parser options.argv = sys.argv if options.info or options.info_all else None @@ -966,11 +968,13 @@ def clean_opts(options): if options.tab_side == "full" and options.tab_name_align == "edge": # This case does not make sense since there are two tab edges in this case. So picking left edge. - print("** Warning: Aligning card name as 'left' for 'full' tabs **") + logger.warning( + "Tab side 'full' and tab name align 'edge' are incompatible. Aligning card name as 'left' for 'full' tabs" + ) options.tab_name_align = "left" if options.tab_number < 1: - print("** Warning: --tab-number must be 1 or greater. Setting to 1. **") + logger.warning("--tab-number must be 1 or greater. Setting to 1.") options.tab_number = 1 if options.tab_side == "full" and options.tab_number != 1: @@ -978,16 +982,16 @@ def clean_opts(options): if "-alternate" in options.tab_side: if options.tab_number != 2: - print( - "** Warning: --tab-side with 'alternate' implies 2 tabs. Setting --tab-number to 2 **" + logger.warning( + "--tab-side with 'alternate' implies 2 tabs. Setting --tab-number to 2." ) options.tab_number = 2 # alternating left and right, so override tab_number if "-flip" in options.tab_side: # for left and right tabs if options.tab_number != 2: - print( - "** Warning: --tab-side with 'flip' implies 2 tabs. Setting --tab-number to 2 **" + logger.warning( + "--tab-side with 'flip' implies 2 tabs. Setting --tab-number to 2." ) options.tab_number = ( 2 # alternating left and right with a flip, so override tab_number @@ -997,7 +1001,7 @@ def clean_opts(options): options.flip = False if options.tab_number < 3 and options.tab_serpentine: - print("** Warning: --tab-serpentine only valid if --tab-number > 2. **") + logger.warning("--tab-serpentine only valid if --tab-number > 2.") options.tab_serpentine = False if options.cost is None: @@ -1244,11 +1248,9 @@ def parse_papersize(spec): except AttributeError: try: paperwidth, paperheight = parseDimensions(papersize) - print( + logger.info( ( - "Using custom paper size, {:.2f}cm x {:.2f}cm".format( - paperwidth / cm, paperheight / cm - ) + f"Using custom paper size, {paperwidth / cm:.2f}cm x {paperheight / cm:.2f}cm" ) ) except ValueError: @@ -1260,29 +1262,23 @@ def parse_cardsize(spec, sleeved): spec = spec.upper() if spec == "SLEEVED" or sleeved: dominionCardWidth, dominionCardHeight = (9.4 * cm, 6.15 * cm) - print( + logger.info( ( - "Using sleeved card size, {:.2f}cm x {:.2f}cm".format( - dominionCardWidth / cm, dominionCardHeight / cm - ) + f"Using sleeved card size, {dominionCardWidth / cm:.2f}cm x {dominionCardHeight / cm:.2f}cm" ) ) elif spec in ["NORMAL", "UNSLEEVED"]: dominionCardWidth, dominionCardHeight = (9.1 * cm, 5.9 * cm) - print( + logger.info( ( - "Using normal card size, {:.2f}cm x{:.2f}cm".format( - dominionCardWidth / cm, dominionCardHeight / cm - ) + f"Using normal card size, {dominionCardWidth / cm:.2f}cm x{dominionCardHeight / cm:.2f}cm" ) ) else: dominionCardWidth, dominionCardHeight = parseDimensions(spec) - print( + logger.info( ( - "Using custom card size, {:.2f}cm x {:.2f}cm".format( - dominionCardWidth / cm, dominionCardHeight / cm - ) + f"Using custom card size, {dominionCardWidth / cm:.2f}cm x {dominionCardHeight / cm:.2f}cm" ) ) return dominionCardWidth, dominionCardHeight @@ -1415,7 +1411,7 @@ def read_card_data(options): Estate_index = find_index_of_object(cards, {"card_tag": "Estate"}) if Copper_index is None or Estate_index is None or StartDeck_index is None: # Something is wrong, can't find one or more of the cards that need to change - print("Error - cannot create Start Decks") + logger.warning("Cannot create Start Decks") # Remove the Start Deck prototype if we can if StartDeck_index is not None: @@ -1483,6 +1479,11 @@ def __init__(self, order, lang, baseCards): # Create a sort collator based on the selected language. Will be used the generate the sort keys. self.collator = Collator.createInstance(Locale(lang)) else: + logger.warning( + "PyICU library not found. The dividers will be ordered by default sort key (might not be the " + "correct alphabetical order for the selected language)." + ) + self.collator = None if order == "global": @@ -1860,13 +1861,7 @@ def filter_sort_cards(cards, options): # Give indication if an imput did not match anything unknownExpansions = options.expansions - knownExpansions if unknownExpansions: - print( - ( - "Error - unknown expansion(s): {}".format( - ", ".join(unknownExpansions) - ) - ) - ) + logger.warning((f"Unknown expansion(s): {', '.join(unknownExpansions)}")) # Take care of fan expansions. Fan expansions must be explicitly named to be added. # If no --fan is given, then no fan cards are added. @@ -1892,12 +1887,8 @@ def filter_sort_cards(cards, options): # Give indication if an imput did not match anything unknownExpansions = options.fan - knownExpansions if unknownExpansions: - print( - ( - "Error - unknown expansion(s): {}".format( - ", ".join(unknownExpansions) - ) - ) + logger.warning( + (f"Unknown fan expansion(s): {', '.join(unknownExpansions)}") ) if options.exclude_expansions: @@ -1923,9 +1914,8 @@ def filter_sort_cards(cards, options): # Give indication if an imput did not match anything unknownExpansions = options.exclude_expansions - knownExpansions if unknownExpansions: - print( - "Error - unknown exclude expansion(s): %s" - % ", ".join(unknownExpansions) + logger.warning( + f"Unknown exclude expansion(s): {', '.join(unknownExpansions)}" ) # Now keep only the cards that are in the sets that have been requested @@ -2131,25 +2121,18 @@ def generate(options): dd = calculate_layout(options, cards) - print( - "Paper dimensions: {:.2f}cm (w) x {:.2f}cm (h)".format( - options.paperwidth / cm, options.paperheight / cm - ) + logger.info( + f"Paper dimensions: {options.paperwidth / cm:.2f}cm (w) x {options.paperheight / cm:.2f}cm (h)".format ) - print( - "Tab dimensions: {:.2f}cm (w) x {:.2f}cm (h)".format( - options.dividerWidthReserved / cm, options.dividerHeightReserved / cm - ) + logger.info( + f"Tab dimensions: {options.dividerWidthReserved / cm:.2f}cm (w) " + f"x {options.dividerHeightReserved / cm:.2f}cm (h)" ) - print( - "{} dividers horizontally, {} vertically".format( - options.numDividersHorizontal, options.numDividersVertical - ) + logger.info( + f"{options.numDividersHorizontal} dividers horizontally, {options.numDividersVertical} vertically" ) - print( - "Margins: {:.2f}cm h, {:.2f}cm v\n".format( - options.horizontalMargin / cm, options.verticalMargin / cm - ) + logger.info( + f"Margins: {options.horizontalMargin / cm:.2f}cm h, {options.verticalMargin / cm:.2f}cm v" ) dd.draw(cards) @@ -2157,6 +2140,9 @@ def generate(options): def main(): options = parse_opts() + logger.remove() + logger.add(sys.stderr, level=options.log_level) + options = clean_opts(options) if options.preview: fname = "{}.{}".format(os.path.splitext(options.outfile)[0], "png")