diff --git a/.gitignore b/.gitignore index c9e10e39..2b4e7163 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ dist .tox/ venv + +/local_fonts/ diff --git a/dev-requirements.txt b/dev-requirements.txt index e92f1c77..e4df8f2a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,7 +4,7 @@ # # pip-compile --extra=dev --no-emit-index-url --output-file=dev-requirements.txt # -backports-tarfile==1.0.0 +backports-tarfile==1.1.1 # via jaraco-context build==1.2.1 # via domdiv (pyproject.toml) @@ -22,17 +22,17 @@ configargparse==1.7 # via domdiv (pyproject.toml) distlib==0.3.8 # via virtualenv -docutils==0.20.1 +docutils==0.21.2 # via readme-renderer doit==0.36.0 # via domdiv (pyproject.toml) -exceptiongroup==1.2.0 +exceptiongroup==1.2.1 # via pytest -filelock==3.13.3 +filelock==3.14.0 # via virtualenv -identify==2.5.35 +identify==2.5.36 # via pre-commit -idna==3.6 +idna==3.7 # via requests importlib-metadata==7.1.0 # via @@ -46,10 +46,12 @@ jaraco-classes==3.4.0 # via keyring jaraco-context==5.3.0 # via keyring -jaraco-functools==4.0.0 +jaraco-functools==4.0.1 # via keyring -keyring==25.1.0 +keyring==25.2.0 # via twine +loguru==0.7.2 + # via domdiv (pyproject.toml) markdown-it-py==3.0.0 # via rich mdurl==0.1.2 @@ -72,9 +74,9 @@ pillow==10.3.0 # reportlab pkginfo==1.10.0 # via twine -platformdirs==4.2.0 +platformdirs==4.2.1 # via virtualenv -pluggy==1.4.0 +pluggy==1.5.0 # via pytest pre-commit==3.7.0 # via domdiv (pyproject.toml) @@ -82,15 +84,15 @@ pygments==2.17.2 # via # readme-renderer # rich -pyproject-hooks==1.0.0 +pyproject-hooks==1.1.0 # via build -pytest==8.1.1 +pytest==8.2.0 # via domdiv (pyproject.toml) pyyaml==6.0.1 # via pre-commit readme-renderer==43.0 # via twine -reportlab==4.1.0 +reportlab==4.2.0 # via domdiv (pyproject.toml) requests==2.31.0 # via @@ -105,7 +107,6 @@ rich==13.7.1 tomli==2.0.1 # via # build - # pyproject-hooks # pytest twine==5.0.0 # via domdiv (pyproject.toml) @@ -113,7 +114,7 @@ urllib3==2.2.1 # via # requests # twine -virtualenv==20.25.1 +virtualenv==20.26.1 # via pre-commit zipp==3.18.1 # via importlib-metadata diff --git a/requirements.txt b/requirements.txt index 620ad327..c57be7f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,11 @@ chardet==5.2.0 # via reportlab configargparse==1.7 # via domdiv (pyproject.toml) +loguru==0.7.2 + # via domdiv (pyproject.toml) pillow==10.3.0 # via # domdiv (pyproject.toml) # reportlab -reportlab==4.1.0 +reportlab==4.2.0 # via domdiv (pyproject.toml) diff --git a/src/domdiv/config_options.py b/src/domdiv/config_options.py new file mode 100644 index 00000000..f14fa3e1 --- /dev/null +++ b/src/domdiv/config_options.py @@ -0,0 +1,1146 @@ +import os +import sys + +import configargparse +import reportlab.lib.pagesizes as pagesizes +from loguru import logger +from reportlab.lib.units import cm + +from . import db + +LOCATION_CHOICES = ["tab", "body-top", "hide"] +NAME_ALIGN_CHOICES = ["left", "right", "centre", "edge"] +TAB_SIDE_CHOICES = [ + "left", + "right", + "left-alternate", + "right-alternate", + "left-flip", + "right-flip", + "centre", + "full", +] +TEXT_CHOICES = ["card", "rules", "blank"] +LINE_CHOICES = ["line", "dot", "cropmarks", "line-cropmarks", "dot-cropmarks"] + +HEAD_CHOICES = ["tab", "strap", "cover", "none"] +TAIL_CHOICES = ["tab", "strap", "cover", "folder", "none"] +FACE_CHOICES = ["front", "back"] +SPINE_CHOICES = ["name", "types", "tab", "blank"] + +EDITION_CHOICES = ["1", "2", "latest", "upgrade", "removed", "all"] + +ORDER_CHOICES = ["expansion", "global", "colour", "cost"] + +EXPANSION_GLOBAL_GROUP = "extras" + + +def add_opt(options, option, value): + assert not hasattr(options, option) + setattr(options, option, value) + + +def parse_opts(cmdline_args=None): + parser = configargparse.ArgParser( + formatter_class=configargparse.ArgumentDefaultsHelpFormatter, + description="Generate Dominion Dividers", + epilog="Source can be found at 'https://github.com/sumpfork/dominiontabs'. " + "An online version can be found at 'http://domdiv.bgtools.net/'. ", + ) + + # Basic Divider Information + group_basic = parser.add_argument_group( + "Basic Divider Options", "Basic choices for the dividers." + ) + group_basic.add_argument( + "--outfile", + "-o", + dest="outfile", + default="dominion_dividers.pdf", + help="The output file name.", + ) + group_basic.add_argument( + "--papersize", + dest="papersize", + default=None, + help="The size of paper to use; '<%%f>x<%%f>' (size in cm), or 'A4', or 'LETTER'. " + "If not specified, it will default to system defaults, and if the system defaults " + "are not found, then to 'LETTER'.", + ) + group_basic.add_argument( + "--language", + "-l", + dest="language", + default=db.LANGUAGE_DEFAULT, + choices=db.get_languages(), + help="Language of divider text.", + ) + group_basic.add_argument( + "--font-dir", + help="A directory path to scan for font files, preferring them over fonts in the domdiv package", + ) + group_basic.add_argument( + "--orientation", + choices=["horizontal", "vertical"], + dest="orientation", + default="horizontal", + help="Either horizontal or vertical divider orientation.", + ) + group_basic.add_argument( + "--size", + dest="size", + default="normal", + help="Dimensions of the cards to use with the dividers '<%%f>x<%%f>' (size in cm), " + "or 'normal' = '9.1x5.9', or 'sleeved' = '9.4x6.15'.", + ) + group_basic.add_argument( + "--sleeved", action="store_true", dest="sleeved", help="Same as --size=sleeved." + ) + group_basic.add_argument( + "--order", + choices=ORDER_CHOICES, + default="expansion", + dest="order", + help="Sort order for the dividers: " + " 'global' will sort by card name;" + " 'expansion' will sort by expansion, then card name;" + " 'colour' will sort by card type, then card name;" + " 'cost' will sort by expansion, then card cost, then name.", + ) + + # Divider Body + group_body = parser.add_argument_group( + "Divider Body", "Changes what is displayed on the body of the dividers." + ) + group_body.add_argument( + "--front", + choices=TEXT_CHOICES, + dest="text_front", + default="card", + help="Text to print on the front of the divider; " + "'card' will print the text from the game card; " + "'rules' will print additional rules for the game card; " + "'blank' will not print text on the divider.", + ) + group_body.add_argument( + "--back", + choices=TEXT_CHOICES + ["none"], + dest="text_back", + default="rules", + help="Text to print on the back of the divider; " + "'card' will print the text from the game card; " + "'rules' will print additional rules for the game card; " + "'blank' will not print text on the divider; " + "'none' will prevent the back pages from printing. ", + ) + group_body.add_argument( + "--count", + action="store_true", + dest="count", + help="Display the card count on the body of card dividers " + "and the randomizer count on the body of expansion dividers.", + ) + group_body.add_argument( + "--types", + action="store_true", + dest="types", + help="Display card type on the body of the divider.", + ) + + # Divider Tab + group_tab = parser.add_argument_group( + "Divider Tab", "Changes what is displayed on on the Divider Tab." + ) + group_tab.add_argument( + "--tab-side", + choices=TAB_SIDE_CHOICES, + dest="tab_side", + default="right-alternate", + help="Alignment of tab; " + "'left'/'right'/'centre' sets the starting side of the tabs; " + "'full' will force all label tabs to be full width of the divider; sets --tab_number 1 " + "'left-alternate' will start on the left and then toggle between left and right for the tabs," + " sets --tab_number 2; " + "'right-alternate' will start on the right and then toggle between right and left for the tabs," + " sets --tab_number 2; " + "'left-flip' like left-alternate, but the right will be flipped front/back with tab on left," + " sets --tab_number 2; " + "'right-flip' like right-alternate, but the left will be flipped front/back with tab on right," + " sets --tab_number 2; ", + ) + group_tab.add_argument( + "--tab-number", + type=int, + 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 " + "of the tabs will be evenly spaced until ending on the opposite side. Then the cycle repeats. " + "May be overriden by some options of --tab_side.", + ) + group_tab.add_argument( + "--tab-serpentine", + action="store_true", + help="Affects the order of tabs. When not selected, tabs will progress from the starting side (left/right) " + "to the opposite side (right/left), and then repeat (e.g., left to right, left to right, etc.). " + "When selected, the order is changed to smoothly alternate between the two sides " + "(e.g., left to right, to left, to right, etc.) " + "Only valid if --tab_number > 2.", + ) + group_tab.add_argument( + "--tab-name-align", + choices=NAME_ALIGN_CHOICES + ["center"], + dest="tab_name_align", + default="left", + help="Alignment of text on the tab; " + "The 'edge' option will align the card name to the outside edge of the " + "tab, so that when using tabs on alternating sides, " + "the name is less likely to be hidden by the tab in front " + "(edge will revert to left when tab_side is full since there is no edge in that case).", + ) + group_tab.add_argument( + "--tabwidth", + type=float, + default=4.0, + help="Width in cm of stick-up tab (ignored if --tab_side is 'full' or --tabs_only is used).", + ) + group_tab.add_argument( + "--cost", + action="append", + choices=LOCATION_CHOICES, + help="Where to display the card cost; may be set to " + "'hide' to indicate it should not be displayed, or " + "given multiple times to show it in multiple places. " + "(If not given, will default to 'tab'.)", + ) + group_tab.add_argument( + "--set-icon", + action="append", + choices=LOCATION_CHOICES, + help="Where to display the set icon; may be set to " + "'hide' to indicate it should not be displayed, or " + "given multiple times to show it in multiple places. " + "(If not given, will default to 'tab'.)", + ) + group_tab.add_argument( + "--no-tab-artwork", + action="store_true", + dest="no_tab_artwork", + help="Don't show background artwork on tabs.", + ) + group_tab.add_argument( + "--tab-artwork-opacity", + type=float, + default=1.0, + help="Multiply opacity of tab background art by this value; " + "can be used to make text show up clearer on dark backrounds, " + "particularly on printers that output darker than average", + ) + group_tab.add_argument( + "--tab-artwork-resolution", + type=int, + default=0, + help="Limit the DPI resolution of tab background art. " + "If nonzero, any higher-resolution images will be resized to " + "reduce output file size.", + ) + group_tab.add_argument( + "--use-text-set-icon", + action="store_true", + dest="use_text_set_icon", + help="Use text/letters to represent a card's set instead of the set icon.", + ) + group_tab.add_argument( + "--use-set-icon", + action="store_true", + dest="use_set_icon", + help="Use set icon instead of a card icon. Applies to Promo cards.", + ) + group_tab.add_argument( + "--expansion-reset-tabs", + action="store_true", + dest="expansion_reset_tabs", + help="When set, the tabs are restarted (left/right) at the beginning of each expansion. " + "If not set, the tab pattern will continue from one expansion to the next. ", + ) + + # Expanion Dividers + group_expansion = parser.add_argument_group( + "Expansion Dividers", "Adding separator dividers for each expansion." + ) + group_expansion.add_argument( + "--expansion-dividers", + action="store_true", + dest="expansion_dividers", + help="Add dividers describing each expansion set. " + "A list of cards in the expansion will be shown on the front of the divider.", + ) + group_expansion.add_argument( + "--centre-expansion-dividers", + action="store_true", + dest="centre_expansion_dividers", + help="Centre the tabs on expansion dividers (same width as dividers.)", + ) + group_expansion.add_argument( + "--full-expansion-dividers", + action="store_true", + dest="full_expansion_dividers", + help="Full width expansion dividers.", + ) + group_expansion.add_argument( + "--expansion-dividers-long-name", + action="store_true", + dest="expansion_dividers_long_name", + help="Use the long name with edition information on the expansion divider tab. " + "Without this, the shorter expansion name is used on the expansion divider tab.", + ) + + # Divider Selection + group_select = parser.add_argument_group( + "Divider Selection", "What expansions are used, and grouping of dividers." + ) + group_select.add_argument( + "--expansions", + "--expansion", + nargs="*", + action="append", + dest="expansions", + help="Limit dividers to only the specified expansions. " + "If no limits are set, then the latest expansions are included. " + "Expansion names can also be given in the language specified by " + "the --language parameter. Any expansion with a space in the name must " + "be enclosed in double quotes. This may be called multiple times. " + "Values are not case sensitive. Wildcards may be used: " + "'*' any number of characters, '?' matches any single character, " + "'[seq]' matches any character in seq, and '[!seq]' matches any character not in seq. " + "For example, 'dominion*' will match all expansions that start with 'dominion'. " + "Choices available in all languages include: {}".format( + ", ".join("%s" % x for x in db.get_expansions()[0]) + ), + ) + group_select.add_argument( + "--fan", + nargs="*", + action="append", + dest="fan", + help="Add dividers from the specified fan made expansions. " + "If this option is not used, no fan expansions will be included. " + "Fan made expansion names can also be given in the language specified by " + "the --language parameter. Any fan expansion with a space in the name must " + "be enclosed in double quotes. This may be called multiple times. " + "Values are not case sensitive. Wildcards may be used: " + "'*' any number of characters, '?' matches any single character, " + "'[seq]' matches any character in seq, and '[!seq]' matches any character not in seq. " + "Choices available in all languages include: {}".format( + ", ".join("%s" % x for x in db.get_expansions()[1]) + ), + ) + group_select.add_argument( + "--exclude-expansions", + "--exclude-expansion", + nargs="*", + action="append", + metavar="EXCLUDED", + dest="exclude_expansions", + help="Limit dividers to not include the specified expansions. " + "Useful if you want all the expansions, except for one or two. " + "If an expansion is explicitly specified with both '--expansion' and " + "'--exclude-expansion', then '--exclude-expansion' wins, and the " + "expansion is NOT included. Expansion names can also be given in the " + "language specified by the --language parameter. Any expansion with a " + "space in the name must be enclosed in double quotes. This may be " + "called multiple times. Values are not case sensitive. Wildcards may " + "be used. See the help for '--expansion' for details on wildcards. May " + "be the name of an official expansion or fan expansion - see the help " + "for --expansion and --fan for a list of possible names.", + ) + group_select.add_argument( + "--edition", + choices=EDITION_CHOICES, + dest="edition", + help="Editions to include: " + "'1' is for all 1st Editions; " + "'2' is for all 2nd Editions; " + "'upgrade' is for all upgrade cards for each expansion; " + "'removed' is for all removed cards for each expansion; " + "'latest' is for the latest edition for each expansion; " + "'all' is for all editions of expansions, upgrade cards, and removed cards; " + " This can be combined with other options to refine the expansions to include in the output." + " (default: all)", + ) + group_select.add_argument( + "--upgrade-with-expansion", + action="store_true", + dest="upgrade_with_expansion", + help="Include any new edition upgrade cards with the expansion being upgraded.", + ) + group_select.add_argument( + "--base-cards-with-expansion", + action="store_true", + help="Print the base cards as part of the expansion (i.e., a divider for 'Silver' " + "will be printed as both a 'Dominion' card and as an 'Intrigue 1st Edition' card). " + "If this option is not given, all base cards are placed in their own 'Base' expansion.", + ) + group_select.add_argument( + "--group-special", + "--special-card-groups", + action="store_true", + dest="group_special", + help="Group cards that generally are used together " + "(e.g., Shelters, Tournament and Prizes, Urchin/Mercenary, etc.).", + ) + group_select.add_argument( + "--group-kingdom", + action="store_true", + dest="group_kingdom", + help="Group cards that have randomizers into the expansion, " + "and those that don't have randomizers into the expansion's 'Extra' section.", + ) + group_select.add_argument( + "--group-global", + nargs="*", + action="append", + dest="group_global", + help="Group all cards of the specified types across all expansions into one 'Extras' divider. " + "This may be called multiple times. Values are not case sensitive. " + "Choices available include: {}".format( + ", ".join("%s" % x for x in db.get_global_groups()[1]) + ), + ) + group_select.add_argument( + "--no-trash", + action="store_true", + dest="no_trash", + help="Exclude Trash from cards.", + ) + group_select.add_argument( + "--curse10", + action="store_true", + dest="curse10", + help="Package Curse cards into groups of ten cards.", + ) + group_select.add_argument( + "--start-decks", + action="store_true", + dest="start_decks", + help="Include four start decks with the Base cards.", + ) + group_select.add_argument( + "--include-blanks", + type=int, + default=0, + help="Number of blank dividers to include.", + ) + group_select.add_argument( + "--exclude-events", + action="store_true", + help="Group all 'Event' cards across all expansions into one divider." + "Same as '--group-global Events'", + ) + group_select.add_argument( + "--exclude-landmarks", + action="store_true", + help="Group all 'Landmark' cards across all expansions into one divider." + "Same as '--group-global landmarks'", + ) + group_select.add_argument( + "--exclude-projects", + action="store_true", + help="Group all 'Project' cards across all expansions into one divider." + "Same as '--group-global projects'", + ) + group_select.add_argument( + "--exclude-ways", + action="store_true", + help="Group all 'Way' cards across all expansions into one divider." + "Same as '--group-global ways'", + ) + group_select.add_argument( + "--exclude-traits", + action="store_true", + help="Group all 'Trait' cards across all expansions into one divider." + "Same as '--group-global traits'", + ) + group_select.add_argument( + "--only-type-any", + "--only-type", + "--type-any", + nargs="*", + action="append", + dest="only_type_any", + help="Limit dividers to only those with the specified types. " + "A divider is kept if ANY of the provided types are associated with the divider. " + "Default is all types are included. " + "Any type with a space in the name must be enclosed in double quotes. " + "Values are not case sensitive. " + "Choices available in all languages include: {}".format( + ", ".join("%s" % x for x in db.get_types()) + ), + ) + group_select.add_argument( + "--only-type-all", + "--type-all", + nargs="*", + action="append", + dest="only_type_all", + help="Limit dividers to only those with the specified types. " + "A divider is kept if ALL of the provided types are associated with the divider. " + "Any type with a space in the name must be enclosed in double quotes. " + "Values are not case sensitive. " + "Choices available in all languages include: {}".format( + ", ".join("%s" % x for x in db.get_types()) + ), + ) + + # Divider Sleeves/Wrappers + group_wrapper = parser.add_argument_group( + "Card Sleeves/Wrappers", "Generating dividers that are card sleeves/wrappers." + ) + group_wrapper.add_argument( + "--wrapper", + action="store_true", + dest="wrapper_meta", + help="Draw sleeves (wrappers) instead of dividers for the cards. " + "Same as --head=strap --tail=folder", + ) + group_wrapper.add_argument( + "--pull-tab", + action="store_true", + dest="pull_tab_meta", + help="Draw folding pull tabs instead of dividers for the cards. " + "Same as --head=tab --tail=cover", + ) + group_wrapper.add_argument( + "--tent", + action="store_true", + dest="tent_meta", + help="Draw folding tent covers instead of dividers for the cards. " + "Same as --head=cover --head-facing=back --head-text=back " + "--tail=tab --tail-facing=front", + ) + group_wrapper.add_argument( + "--head", + choices=HEAD_CHOICES, + dest="head", + default="tab", + help="Top tab or wrapper type: " + "'tab' for divider tabs, " + "'strap' for longer folding tabs, " + "'cover' for matchbook-style folding covers, " + "or 'none' to leave the top edge plain. " + "The folding options create a top spine that you can customize " + "with --spine.", + ) + group_wrapper.add_argument( + "--tail", + choices=TAIL_CHOICES, + dest="tail", + default="none", + help="Bottom tab or wrapper type: " + "'tab' for a bottom tab banner, " + "'strap' for a pull tab under the cards, " + "'cover' for a simple back cover, " + "'folder' to create tab folders, " + "or 'none' to leave the bottom edge plain.", + ) + group_wrapper.add_argument( + "--head-facing", + choices=FACE_CHOICES, + dest="head_facing", + default="front", + help="Text orientation for top tabs and wrappers: " + "'front' shows the text upright when flat, " + "'back' shows it upright when folded over.", + ) + group_wrapper.add_argument( + "--tail-facing", + choices=FACE_CHOICES, + dest="tail_facing", + default="back", + help="Text orientation for tail wrappers: " + "'front' shows the text upright when flat, " + "'back' shows it upright when folded under.", + ) + group_wrapper.add_argument( + "--head-text", + choices=TEXT_CHOICES + FACE_CHOICES, + dest="head_text", + default="blank", + help="Text to print on top cover panels: " + "'card' shows the text from the game card, " + "'rules' shows additional rules for the game card, " + "'blank' leaves the panel blank; " + "'front' uses the same setting as --front; " + "'back' uses the same setting as --back.", + ) + group_wrapper.add_argument( + "--tail-text", + choices=TEXT_CHOICES + FACE_CHOICES, + dest="tail_text", + default="back", + help="Text to print on bottom folder panels: " + "'card' shows the text from the game card, " + "'rules' shows additional rules for the game card, " + "'blank' leaves the panel blank; " + "'front' uses the same setting as --front; " + "'back' uses the same setting as --back.", + ) + group_wrapper.add_argument( + "--head-height", + type=float, + default=0.0, + help="Height of the top panel in centimeters " + "(a value of 0 uses tab height or card height as appropriate).", + ) + group_wrapper.add_argument( + "--tail-height", + type=float, + default=0.0, + help="Height of the bottom panel in centimeters " + "(a value of 0 uses tab height or card height as appropriate).", + ) + group_wrapper.add_argument( + "--spine", + choices=SPINE_CHOICES, + dest="spine", + default="name", + help="Text to print on the spine of top covers: " + "'name' prints the card name; " + "'type' prints the card type; " + "'tab' prints tab text and graphics; " + "'blank' leaves the spine blank. " + "This is only valid with folding --head options.", + ) + group_wrapper.add_argument( + "--thickness", + type=float, + default=2.0, + help="Thickness of a stack of 60 cards (Copper) in centimeters. " + "Typically unsleeved cards are 2.0, thin sleeved cards are 2.4, and thick sleeved cards are 3.2. " + "This is only valid with --wrapper or other folding options.", + ) + group_wrapper.add_argument( + "--sleeved-thick", + action="store_true", + dest="sleeved_thick", + help="Same as --size=sleeved --thickness 3.2.", + ) + group_wrapper.add_argument( + "--sleeved-thin", + action="store_true", + dest="sleeved_thin", + help="Same as --size=sleeved --thickness 2.4.", + ) + group_wrapper.add_argument( + "--notch", + action="store_true", + dest="notch", + help="Creates thumb notches opposite to the divider tabs, " + "which can make it easier to remove cards from wrappers or stacks. " + "Equivalent to --notch-length=1.5 --notch-height=0.25", + ) + group_wrapper.add_argument( + "--notch-length", + type=float, + default=0.0, + help="Sets the length of thumb notches in centimeters.", + ) + group_wrapper.add_argument( + "--notch-height", + type=float, + default=0.0, + help="Sets the height of thumb notches in centimeters.", + ) + + # Printing + group_printing = parser.add_argument_group( + "Printing", "Changes how the Dividers are printed." + ) + group_printing.add_argument( + "--minmargin", + dest="minmargin", + default="1x1", + help="Page margin in cm in the form '<%%f>x<%%f>', left/right x top/bottom).", + ) + group_printing.add_argument( + "--cropmarks", + action="store_true", + dest="cropmarks", + help="Print crop marks on both sides, rather than tab outlines on the front side.", + ) + group_printing.add_argument( + "--linewidth", + type=float, + default=0.1, + help="Width of lines for card outlines and crop marks.", + ) + group_printing.add_argument( + "--front-offset", + type=float, + dest="front_offset", + default=0, + help="Front page horizontal offset points to shift to the right. Only needed for some printers.", + ) + group_printing.add_argument( + "--front-offset-height", + type=float, + dest="front_offset_height", + default=0, + help="Front page vertical offset points to shift upward. Only needed for some printers.", + ) + group_printing.add_argument( + "--back-offset", + type=float, + dest="back_offset", + default=0, + help="Back page horizontal offset points to shift to the right. Only needed for some printers.", + ) + group_printing.add_argument( + "--back-offset-height", + type=float, + dest="back_offset_height", + default=0, + help="Back page vertical offset points to shift upward. Only needed for some printers.", + ) + group_printing.add_argument( + "--vertical-gap", + type=float, + default=0.0, + help="Vertical gap between dividers in centimeters.", + ) + group_printing.add_argument( + "--horizontal-gap", + type=float, + default=0.0, + help="Horizontal gap between dividers in centimeters.", + ) + group_printing.add_argument( + "--no-page-footer", + action="store_true", + dest="no_page_footer", + help="Do not print the expansion name at the bottom of the page.", + ) + group_printing.add_argument( + "--num-pages", + type=int, + default=-1, + help="Stop generating dividers after this many pages, -1 for all.", + ) + group_printing.add_argument( + "--tabs-only", + action="store_true", + dest="tabs_only", + help="Draw only the divider tabs and no divider outlines. " + "Used to print the divider tabs on labels.", + ) + group_printing.add_argument( + "--black-tabs", + action="store_true", + help="In tabs-only mode, draw tabs on black background", + ) + group_printing.add_argument( + "--linetype", + choices=LINE_CHOICES, + dest="linetype", + default="line", + help="The divider outline type. " + "'line' will print a solid line outlining the divider; " + "'dot' will print a dot at each corner of the divider; " + "'cropmarks' will print cropmarks for the divider; " + "'line-cropmarks' will combine 'line' and 'cropmarks'; " + "'dot-cropmarks' will combine 'dot' and 'cropmarks'", + ) + group_printing.add_argument( + "--cropmarkLength", + type=float, + default=0.2, + help="Length of actual drawn cropmark in centimeters.", + ) + group_printing.add_argument( + "--cropmarkSpacing", + type=float, + default=0.1, + help="Spacing between card and the start of the cropmark in centimeters.", + ) + group_printing.add_argument( + "--rotate", + type=int, + choices=[0, 90, 180, 270], + default=0, + help="Divider degrees of rotation relative to the page edge. " + "No optimization will be done on the number of dividers per page.", + ) + group_printing.add_argument( + "--label", + dest="label_name", + choices=db.get_label_data()[3], + default=None, + help="Use preset label dimentions. Specify a label name. " + "This will override settings that conflict with the preset label settings.", + ) + group_printing.add_argument( + "--info", + action="store_true", + dest="info", + help="Add a page that has all the options used for the file.", + ) + group_printing.add_argument( + "--info-all", + action="store_true", + dest="info_all", + help="Same as --info, but includes pages with all the possible options that can be used.", + ) + group_printing.add_argument( + "--preview", + action="store_true", + help="Only generate a preview png image of the first page", + ) + group_printing.add_argument( + "--preview-resolution", + type=int, + default=150, + help="resolution in DPI to render preview at, for --preview option", + ) + # Special processing + group_special = parser.add_argument_group( + "Miscellaneous", "These options are generally not used." + ) + group_special.add_argument( + "--cardlist", + dest="cardlist", + help="Path to file that enumerates each card on its own line to be included or excluded." + " To include a card, add its card name on a line. The name can optionally be preceeded by '+'." + " To exclude a card, add its card name on a line preseeded by a '-'" + " If any card is included by this method, only cards specified in this file will be printed.", + ) + group_special.add_argument( + "-c", + is_config_file=True, + help="Use the specified configuration file to provide options. " + "Command line options override options from this file.", + ) + group_special.add_argument( + "-w", + 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 + options.help = parser.format_help() if options.info_all else None + return options + + +def clean_opts(options): + if "center" in options.tab_side: + options.tab_side = str(options.tab_side).replace("center", "centre") + + if "center" in options.tab_name_align: + options.tab_name_align = str(options.tab_name_align).replace("center", "centre") + + 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. + 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: + 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: + options.tab_number = 1 # Full is 1 big tab + + if "-alternate" in options.tab_side: + if options.tab_number != 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: + 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 + ) + options.flip = True + else: + options.flip = False + + if options.tab_number < 3 and options.tab_serpentine: + logger.warning("--tab-serpentine only valid if --tab-number > 2.") + options.tab_serpentine = False + + if options.cost is None: + options.cost = ["tab"] + + if options.set_icon is None: + options.set_icon = ["tab"] + + if options.sleeved_thick: + options.thickness = 3.2 + options.sleeved = True + + if options.sleeved_thin: + options.thickness = 2.4 + options.sleeved = True + + # if notch is enabled with missing dimensions, provide defaults + notch = options.notch or options.notch_length or options.notch_height + if notch and not options.notch_length: + options.notch_length = 1.5 + if notch and not options.notch_height: + options.notch_height = 0.25 + + if options.cropmarks and options.linetype == "line": + options.linetype = "cropmarks" + + if "cropmarks" in options.linetype: + options.cropmarks = True + + if options.expansions is None: + # No instance given, so default to the latest Official expansions + options.expansions = ["*"] + if options.edition is None: + options.edition = "latest" + else: + # options.expansions is a list of lists. Reduce to single lowercase list + options.expansions = [ + item.lower() for sublist in options.expansions for item in sublist + ] + if "none" in options.expansions: + # keyword to indicate no options. Same as --expansions without any expansions given. + options.expansions = [] + + if options.exclude_expansions: + # options.exclude_expansions is a list of lists. Reduce to single lowercase list + options.exclude_expansions = [ + item.lower() for sublist in options.exclude_expansions for item in sublist + ] + + if options.edition is None: + # set the default + options.edition = "all" + + if options.fan is None: + # No instance given, so default to no Fan expansions + options.fan = [] + else: + # options.fan is a list of lists. Reduce to single lowercase list + options.fan = [item.lower() for sublist in options.fan for item in sublist] + if "none" in options.fan: + # keyword to indicate no options. Same as --fan without any expansions given + options.fan = [] + + if options.only_type_any is None: + # No instance given, so default to empty list + options.only_type_any = [] + else: + # options.only_type_any is a list of lists. Reduce to single lowercase list + options.only_type_any = list( + set([item.lower() for sublist in options.only_type_any for item in sublist]) + ) + + if options.only_type_all is None: + # No instance given, so default to empty list + options.only_type_all = [] + else: + # options.only_type_any is a list of lists. Reduce to single lowercase list + options.only_type_all = list( + set([item.lower() for sublist in options.only_type_all for item in sublist]) + ) + + if options.group_global is None: + options.group_global = [] + elif not any(options.group_global): + # option given with nothing indicates all possible global groupings + options.group_global = db.get_global_groups()[1] + else: + # options.group_global is a list of lists. Reduce to single lowercase list + options.group_global = [ + item.lower() for sublist in options.group_global for item in sublist + ] + # For backwards compatibility + if options.exclude_events: + options.group_global.append("events") + if options.exclude_landmarks: + options.group_global.append("landmarks") + if options.exclude_projects: + options.group_global.append("projects") + if options.exclude_ways: + options.group_global.append("ways") + if options.exclude_traits: + options.group_global.append("traits") + # Remove duplicates from the list + options.group_global = list(set(options.group_global)) + + if options.tabs_only and options.label_name is None: + # default is Avery 8867 + options.label_name = "8867" + + options.label = None + if options.label_name is not None: + for label in db.get_label_data()[0]: + if options.label_name.upper() in [n.upper() for n in label["names"]]: + options.label = label + break + + assert options.label is not None, "Label '{}' not defined".format( + options.label_name + ) + + # Defaults for missing values + label = options.label + label["paper"] = label["paper"] if "paper" in label else "LETTER" + label["tab-only"] = label["tab-only"] if "tab-only" in label else True + label["tab-height"] = ( + label["tab-height"] if "tab-height" in label else label["height"] + ) + label["body-height"] = ( + label["body-height"] + if "body-height" in label + else label["height"] - label["tab-height"] + ) + label["gap-vertical"] = ( + label["gap-vertical"] if "gap-vertical" in label else 0.0 + ) + label["gap-horizontal"] = ( + label["gap-horizontal"] if "gap-horizontal" in label else 0.0 + ) + label["pad-vertical"] = ( + label["pad-vertical"] if "pad-vertical" in label else 0.1 + ) + label["pad-horizontal"] = ( + label["pad-horizontal"] if "pad-horizontal" in label else 0.1 + ) + + # Option Overrides when using labels + MIN_BODY_CM_FOR_COUNT = 0.6 + MIN_BODY_CM_FOR_TEXT = 4.0 + MIN_HEIGHT_CM_FOR_VERTICAL = 5.0 + MIN_WIDTH_CM_FOR_FULL = 5.0 + + options.linewidth = 0.0 + options.cropmarks = False + options.head = "tab" + options.tail = "none" + options.wrapper_meta = options.pull_tab_meta = options.tent_meta = False + options.papersize = label["paper"] + if label["tab-only"]: + options.tabs_only = True + if label["body-height"] < MIN_BODY_CM_FOR_TEXT: + # Not enough room for any text + options.text_front = "blank" + options.text_back = "blank" + if label["body-height"] < MIN_BODY_CM_FOR_COUNT: + # Not enough room for count and type + options.count = False + options.types = False + if label["height"] < MIN_HEIGHT_CM_FOR_VERTICAL: + # Not enough room to make vertical + options.orientation = "horizontal" + if ( + options.label["width"] - 2 * options.label["pad-horizontal"] + ) < MIN_WIDTH_CM_FOR_FULL: + options.tab_side = "full" + options.label = label + + if options.wrapper_meta: + # Same as --head=strap --tail=folder + options.head = "strap" + options.tail = "folder" + if options.pull_tab_meta: + # Same as --head=tab --tail=cover + options.head = "tab" + options.tail = "cover" + if options.tent_meta: + # Same as --head=cover --head-facing=back --head-text=back + # --tail=tab --tail-facing=front + options.head = "cover" + options.head_facing = "back" + options.head_text = "back" + options.tail = "tab" + options.tail_facing = "front" + # Flags set if there's a head wrapper, a tail wrapper, or either + options.headWrapper = options.head in ["strap", "cover", "folder"] + options.tailWrapper = options.tail in ["strap", "cover", "folder"] + options.wrapper = options.headWrapper or options.tailWrapper + + # Expand --head-text and --tail-text if they refer to --front or --back + if options.head_text == "front": + options.head_text = options.text_front + elif options.head_text == "back": + options.head_text = options.text_back + if options.tail_text == "front": + options.tail_text = options.text_front + elif options.tail_text == "back": + options.tail_text = options.text_back + + return options + + +def parse_dimensions(dimensionsStr): + x, y = dimensionsStr.upper().split("X", 1) + return (float(x) * cm, float(y) * cm) + + +def parse_papersize(spec): + papersize = None + if not spec: + if os.path.exists("/etc/papersize"): + papersize = open("/etc/papersize").readline().upper() + else: + papersize = "LETTER" + else: + papersize = spec.upper() + + try: + paperwidth, paperheight = getattr(pagesizes, papersize) + except AttributeError: + try: + paperwidth, paperheight = parse_dimensions(papersize) + logger.info( + ( + f"Using custom paper size, {paperwidth / cm:.2f}cm x {paperheight / cm:.2f}cm" + ) + ) + except ValueError: + paperwidth, paperheight = pagesizes.LETTER + return paperwidth, paperheight + + +def parse_cardsize(spec, sleeved): + spec = spec.upper() + if spec == "SLEEVED" or sleeved: + dominionCardWidth, dominionCardHeight = (9.4 * cm, 6.15 * cm) + logger.info( + ( + 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) + logger.info( + ( + f"Using normal card size, {dominionCardWidth / cm:.2f}cm x{dominionCardHeight / cm:.2f}cm" + ) + ) + else: + dominionCardWidth, dominionCardHeight = parse_dimensions(spec) + logger.info( + ( + f"Using custom card size, {dominionCardWidth / cm:.2f}cm x {dominionCardHeight / cm:.2f}cm" + ) + ) + return dominionCardWidth, dominionCardHeight diff --git a/src/domdiv/db.py b/src/domdiv/db.py new file mode 100644 index 00000000..a69f111c --- /dev/null +++ b/src/domdiv/db.py @@ -0,0 +1,304 @@ +import copy +import functools +import gzip +import json +import os + +import pkg_resources +from loguru import logger + +from . import config_options, db +from .cards import Card, CardType + +EXPANSION_EXTRA_POSTFIX = " extras" + +LANGUAGE_DEFAULT = ( + "en_us" # the primary language used if a language's parts are missing +) +LANGUAGE_XX = "xx" # a dummy language for starting translations + + +@functools.lru_cache() +def get_languages(path="card_db"): + languages = [] + for name in pkg_resources.resource_listdir("domdiv", path): + dir_path = os.path.join(path, name) + if pkg_resources.resource_isdir("domdiv", dir_path): + cards_file = os.path.join(dir_path, f"cards_{name}.json.gz") + sets_file = os.path.join(dir_path, f"sets_{name}.json.gz") + types_file = os.path.join(dir_path, f"types_{name}.json.gz") + if ( + pkg_resources.resource_exists("domdiv", cards_file) + and pkg_resources.resource_exists("domdiv", sets_file) + and pkg_resources.resource_exists("domdiv", types_file) + ): + languages.append(name) + if LANGUAGE_XX in languages: + languages.remove(LANGUAGE_XX) + return languages + + +def get_resource_stream(path): + return gzip.GzipFile( + fileobj=pkg_resources.resource_stream("domdiv", path), + ) + + +@functools.lru_cache() +def get_expansions(): + set_db_filepath = os.path.join("card_db", "sets_db.json.gz") + with get_resource_stream(set_db_filepath) as setfile: + set_file = json.loads(setfile.read().decode("utf-8")) + assert set_file, "Could not load any sets from database" + + fan = [] + official = [] + for s in set_file: + if EXPANSION_EXTRA_POSTFIX not in s: + # Make sure these are set either True or False + set_file[s]["fan"] = set_file[s].get("fan", False) + if set_file[s]["fan"]: + fan.append(s) + else: + official.append(s) + fan.sort() + official.sort() + return official, fan + + +@functools.lru_cache() +def get_global_groups(): + type_db_filepath = os.path.join("card_db", "types_db.json.gz") + with get_resource_stream(type_db_filepath) as typefile: + type_file = json.loads(typefile.read().decode("utf-8")) + assert type_file, "Could not load any card types from database" + + group_global_choices = [] + group_global_valid = [] + for t in type_file: + if "group_global_type" in t: + group_global_valid.append("-".join(t["card_type"]).lower()) + group_global_choices.append(t["group_global_type"].lower()) + group_global_valid.extend(group_global_choices) + group_global_valid.sort() + group_global_choices.sort() + return group_global_choices, group_global_valid + + +@functools.lru_cache() +def get_types(language=LANGUAGE_DEFAULT): + # get a list of valid types + language = language.lower() + type_text_filepath = os.path.join("card_db", language, f"types_{language}.json.gz") + with get_resource_stream(type_text_filepath) as type_text_file: + type_text = json.loads(type_text_file.read().decode("utf-8")) + assert type_text, "Could not load type file for %r" % language + + types = [x.lower() for x in type_text] + types.sort() + return types + + +@functools.lru_cache() +def get_label_data(): + labels_db_filepath = os.path.join("card_db", "labels_db.json.gz") + label_choices = [] + label_keys = [] + label_selections = [] + with get_resource_stream(labels_db_filepath) as labelfile: + label_info = json.loads(labelfile.read().decode("utf-8")) + assert label_info, "Could not load label information from database" + for label in label_info: + if len(label["names"]) > 0: + label_keys.append(label["names"][0]) + label_selections.append( + label["name"] if "name" in label else label["names"][0] + ) + label_choices.extend(label["names"]) + return label_info, label_keys, label_selections, label_choices + + +def find_index_of_object(lst=None, attributes=None): + if lst is None: + lst = [] + if attributes is None: + attributes = {} + # Returns the index of the first object in lst that matches the given attributes. Otherwise returns None. + # attributes is a dict of key: value pairs. Object attributes that are lists are checked to have value in them. + for i, d in enumerate(lst): + # Set match to false just in case there are no attributes. + match = False + for key, value in attributes.items(): + # if anything does not match, then break out and start the next one. + match = hasattr(d, key) + if match: + test = getattr(d, key, None) + if isinstance(test, list): + match = value in test + else: + match = value == test + if not match: + break + + if match: + # If all the attributes are found, then we have a match + return i + + # nothing matched + return None + + +def read_card_data(options): + # Read in the card types + types_db_filepath = os.path.join("card_db", "types_db.json.gz") + with db.get_resource_stream(types_db_filepath) as typefile: + Card.types = json.loads( + typefile.read().decode("utf-8"), object_hook=CardType.decode_json + ) + assert Card.types, "Could not load any card types from database" + + # extract unique types + type_list = [] + for c in Card.types: + type_list = list(set(c.getTypeNames()) | set(type_list)) + # set up the basic type translation. The actual language will be added later. + Card.type_names = {} + for t in type_list: + Card.type_names[t] = t + + # turn Card.types into a dictionary for later + Card.types = dict(((c.getTypeNames(), c) for c in Card.types)) + + # Read in the card database + card_db_filepath = os.path.join("card_db", "cards_db.json.gz") + with get_resource_stream(card_db_filepath) as cardfile: + cards = json.loads( + cardfile.read().decode("utf-8"), object_hook=Card.decode_json + ) + assert cards, "Could not load any cards from database" + + set_db_filepath = os.path.join("card_db", "sets_db.json.gz") + with get_resource_stream(set_db_filepath) as setfile: + Card.sets = json.loads(setfile.read().decode("utf-8")) + assert Card.sets, "Could not load any sets from database" + new_sets = {} + for s in Card.sets: + # Make sure these are set either True or False + Card.sets[s]["no_randomizer"] = Card.sets[s].get("no_randomizer", False) + Card.sets[s]["fan"] = Card.sets[s].get("fan", False) + Card.sets[s]["has_extras"] = Card.sets[s].get("has_extras", True) + Card.sets[s]["upgrades"] = Card.sets[s].get("upgrades", None) + new_sets[s] = Card.sets[s] + # Make an "Extras" set for normal expansions + if Card.sets[s]["has_extras"]: + e = s + db.EXPANSION_EXTRA_POSTFIX + new_sets[e] = copy.deepcopy(Card.sets[s]) + new_sets[e]["set_name"] = "*" + s + db.EXPANSION_EXTRA_POSTFIX + "*" + new_sets[e]["no_randomizer"] = True + new_sets[e]["has_extras"] = False + Card.sets = new_sets + + # Remove the Trash card. Do early before propagating to various sets. + if options.no_trash: + i = find_index_of_object(cards, {"card_tag": "Trash"}) + if i is not None: + del cards[i] + + # Repackage Curse cards into 10 per divider. Do early before propagating to various sets. + if options.curse10: + i = find_index_of_object(cards, {"card_tag": "Curse"}) + if i is not None: + new_cards = [] + cards_remaining = cards[i].getCardCount() + while cards_remaining > 10: + # make a new copy of the card and set count to 10 + new_card = copy.deepcopy(cards[i]) + new_card.setCardCount(10) + new_cards.append(new_card) + cards_remaining -= 10 + + # Adjust original Curse card to the remaining cards (should be 10) + cards[i].setCardCount(cards_remaining) + # Add the new dividers + cards.extend(new_cards) + + # Add any blank cards + if options.include_blanks > 0: + for _ in range(0, options.include_blanks): + c = Card( + card_tag="Blank", + cardset=config_options.EXPANSION_GLOBAL_GROUP, + cardset_tag=config_options.EXPANSION_GLOBAL_GROUP, + cardset_tags=[config_options.EXPANSION_GLOBAL_GROUP], + randomizer=False, + types=("Blank",), + ) + cards.append(c) + + # Create Start Deck dividers. 4 sets. Adjust totals for other cards, too. + # Do early before propagating to various sets. + # The card database contains one prototype divider that needs to be either duplicated or deleted. + if options.start_decks: + # Find the index to the individual cards that need changed in the cards list + StartDeck_index = find_index_of_object(cards, {"card_tag": "Start Deck"}) + Copper_index = find_index_of_object(cards, {"card_tag": "Copper"}) + 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 + logger.warning("Cannot create Start Decks") + + # Remove the Start Deck prototype if we can + if StartDeck_index is not None: + del cards[StartDeck_index] + else: + # Start Deck Constants + STARTDECK_COPPERS = 7 + STARTDECK_ESTATES = 3 + STARTDECK_NUMBER = 4 + + # Add correct card counts to Start Deck prototype. This will be used to make copies. + cards[StartDeck_index].setCardCount(STARTDECK_COPPERS) + cards[StartDeck_index].addCardCount([int(STARTDECK_ESTATES)]) + + # Make new Start Deck Dividers and adjust the corresponding card counts + for x in range(0, STARTDECK_NUMBER): + # Add extra copies of the Start Deck prototype. + # But don't need to add the first one again, since the prototype is already there. + if x > 0: + cards.append(copy.deepcopy(cards[StartDeck_index])) + # Note: By appending, it should not change any of the index values being used + + # Remove Copper and Estate card counts from their dividers + cards[Copper_index].setCardCount( + cards[Copper_index].getCardCount() - STARTDECK_COPPERS + ) + cards[Estate_index].setCardCount( + cards[Estate_index].getCardCount() - STARTDECK_ESTATES + ) + else: + # Remove Start Deck prototype. It is not needed. + StartDeck_index = find_index_of_object(cards, {"card_tag": "Start Deck"}) + if StartDeck_index is not None: + del cards[StartDeck_index] + + # Set cardset_tag and expand cards that are used in multiple sets + new_cards = [] + for card in cards: + sets = list(card.cardset_tags) + if len(sets) > 0: + # Set and save the first one + card.cardset_tag = sets.pop(0) + new_cards.append(card) + for s in sets: + # for the rest, create a copy of the first + if s: + new_card = copy.deepcopy(card) + new_card.cardset_tag = s + new_cards.append(new_card) + cards = new_cards + + # Make sure each card has the right image file. + for card in cards: + card.image = card.setImage() + + return cards diff --git a/src/domdiv/main.py b/src/domdiv/main.py index 421dc31e..0a69394c 100644 --- a/src/domdiv/main.py +++ b/src/domdiv/main.py @@ -1,20 +1,15 @@ -import copy import fnmatch -import functools -import gzip import json import os import sys import unicodedata from collections import Counter, defaultdict -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 +from . import config_options, db +from .cards import Card from .draw import DividerDrawer try: @@ -24,1198 +19,6 @@ except ImportError: have_icu = False -LOCATION_CHOICES = ["tab", "body-top", "hide"] -NAME_ALIGN_CHOICES = ["left", "right", "centre", "edge"] -TAB_SIDE_CHOICES = [ - "left", - "right", - "left-alternate", - "right-alternate", - "left-flip", - "right-flip", - "centre", - "full", -] -TEXT_CHOICES = ["card", "rules", "blank"] -LINE_CHOICES = ["line", "dot", "cropmarks", "line-cropmarks", "dot-cropmarks"] - -HEAD_CHOICES = ["tab", "strap", "cover", "none"] -TAIL_CHOICES = ["tab", "strap", "cover", "folder", "none"] -FACE_CHOICES = ["front", "back"] -SPINE_CHOICES = ["name", "types", "tab", "blank"] - -EDITION_CHOICES = ["1", "2", "latest", "upgrade", "removed", "all"] - -ORDER_CHOICES = ["expansion", "global", "colour", "cost"] - -EXPANSION_GLOBAL_GROUP = "extras" -EXPANSION_EXTRA_POSTFIX = " extras" - -LANGUAGE_DEFAULT = ( - "en_us" # the primary language used if a language's parts are missing -) -LANGUAGE_XX = "xx" # a dummy language for starting translations - - -@functools.lru_cache() -def get_languages(path="card_db"): - languages = [] - for name in pkg_resources.resource_listdir("domdiv", path): - dir_path = os.path.join(path, name) - if pkg_resources.resource_isdir("domdiv", dir_path): - cards_file = os.path.join(dir_path, f"cards_{name}.json.gz") - sets_file = os.path.join(dir_path, f"sets_{name}.json.gz") - types_file = os.path.join(dir_path, f"types_{name}.json.gz") - if ( - pkg_resources.resource_exists("domdiv", cards_file) - and pkg_resources.resource_exists("domdiv", sets_file) - and pkg_resources.resource_exists("domdiv", types_file) - ): - languages.append(name) - if LANGUAGE_XX in languages: - languages.remove(LANGUAGE_XX) - return languages - - -def get_resource_stream(path): - return gzip.GzipFile( - fileobj=pkg_resources.resource_stream("domdiv", path), - ) - - -@functools.lru_cache() -def get_expansions(): - set_db_filepath = os.path.join("card_db", "sets_db.json.gz") - with get_resource_stream(set_db_filepath) as setfile: - set_file = json.loads(setfile.read().decode("utf-8")) - assert set_file, "Could not load any sets from database" - - fan = [] - official = [] - for s in set_file: - if EXPANSION_EXTRA_POSTFIX not in s: - # Make sure these are set either True or False - set_file[s]["fan"] = set_file[s].get("fan", False) - if set_file[s]["fan"]: - fan.append(s) - else: - official.append(s) - fan.sort() - official.sort() - return official, fan - - -@functools.lru_cache() -def get_global_groups(): - type_db_filepath = os.path.join("card_db", "types_db.json.gz") - with get_resource_stream(type_db_filepath) as typefile: - type_file = json.loads(typefile.read().decode("utf-8")) - assert type_file, "Could not load any card types from database" - - group_global_choices = [] - group_global_valid = [] - for t in type_file: - if "group_global_type" in t: - group_global_valid.append("-".join(t["card_type"]).lower()) - group_global_choices.append(t["group_global_type"].lower()) - group_global_valid.extend(group_global_choices) - group_global_valid.sort() - group_global_choices.sort() - return group_global_choices, group_global_valid - - -@functools.lru_cache() -def get_types(language=LANGUAGE_DEFAULT): - # get a list of valid types - language = language.lower() - type_text_filepath = os.path.join("card_db", language, f"types_{language}.json.gz") - with get_resource_stream(type_text_filepath) as type_text_file: - type_text = json.loads(type_text_file.read().decode("utf-8")) - assert type_text, "Could not load type file for %r" % language - - types = [x.lower() for x in type_text] - types.sort() - return types - - -@functools.lru_cache() -def get_label_data(): - labels_db_filepath = os.path.join("card_db", "labels_db.json.gz") - label_choices = [] - label_keys = [] - label_selections = [] - with get_resource_stream(labels_db_filepath) as labelfile: - label_info = json.loads(labelfile.read().decode("utf-8")) - assert label_info, "Could not load label information from database" - for label in label_info: - if len(label["names"]) > 0: - label_keys.append(label["names"][0]) - label_selections.append( - label["name"] if "name" in label else label["names"][0] - ) - label_choices.extend(label["names"]) - return label_info, label_keys, label_selections, label_choices - - -def add_opt(options, option, value): - assert not hasattr(options, option) - setattr(options, option, value) - - -def parse_opts(cmdline_args=None): - parser = configargparse.ArgParser( - formatter_class=configargparse.ArgumentDefaultsHelpFormatter, - description="Generate Dominion Dividers", - epilog="Source can be found at 'https://github.com/sumpfork/dominiontabs'. " - "An online version can be found at 'http://domdiv.bgtools.net/'. ", - ) - - # Basic Divider Information - group_basic = parser.add_argument_group( - "Basic Divider Options", "Basic choices for the dividers." - ) - group_basic.add_argument( - "--outfile", - "-o", - dest="outfile", - default="dominion_dividers.pdf", - help="The output file name.", - ) - group_basic.add_argument( - "--papersize", - dest="papersize", - default=None, - help="The size of paper to use; '<%%f>x<%%f>' (size in cm), or 'A4', or 'LETTER'. " - "If not specified, it will default to system defaults, and if the system defaults " - "are not found, then to 'LETTER'.", - ) - group_basic.add_argument( - "--language", - "-l", - dest="language", - default=LANGUAGE_DEFAULT, - choices=get_languages(), - help="Language of divider text.", - ) - group_basic.add_argument( - "--font-dir", - help="A directory path to scan for font files, preferring them over fonts in the domdiv package", - ) - group_basic.add_argument( - "--orientation", - choices=["horizontal", "vertical"], - dest="orientation", - default="horizontal", - help="Either horizontal or vertical divider orientation.", - ) - group_basic.add_argument( - "--size", - dest="size", - default="normal", - help="Dimensions of the cards to use with the dividers '<%%f>x<%%f>' (size in cm), " - "or 'normal' = '9.1x5.9', or 'sleeved' = '9.4x6.15'.", - ) - group_basic.add_argument( - "--sleeved", action="store_true", dest="sleeved", help="Same as --size=sleeved." - ) - group_basic.add_argument( - "--order", - choices=ORDER_CHOICES, - default="expansion", - dest="order", - help="Sort order for the dividers: " - " 'global' will sort by card name;" - " 'expansion' will sort by expansion, then card name;" - " 'colour' will sort by card type, then card name;" - " 'cost' will sort by expansion, then card cost, then name.", - ) - - # Divider Body - group_body = parser.add_argument_group( - "Divider Body", "Changes what is displayed on the body of the dividers." - ) - group_body.add_argument( - "--front", - choices=TEXT_CHOICES, - dest="text_front", - default="card", - help="Text to print on the front of the divider; " - "'card' will print the text from the game card; " - "'rules' will print additional rules for the game card; " - "'blank' will not print text on the divider.", - ) - group_body.add_argument( - "--back", - choices=TEXT_CHOICES + ["none"], - dest="text_back", - default="rules", - help="Text to print on the back of the divider; " - "'card' will print the text from the game card; " - "'rules' will print additional rules for the game card; " - "'blank' will not print text on the divider; " - "'none' will prevent the back pages from printing. ", - ) - group_body.add_argument( - "--count", - action="store_true", - dest="count", - help="Display the card count on the body of card dividers " - "and the randomizer count on the body of expansion dividers.", - ) - group_body.add_argument( - "--types", - action="store_true", - dest="types", - help="Display card type on the body of the divider.", - ) - - # Divider Tab - group_tab = parser.add_argument_group( - "Divider Tab", "Changes what is displayed on on the Divider Tab." - ) - group_tab.add_argument( - "--tab-side", - choices=TAB_SIDE_CHOICES, - dest="tab_side", - default="right-alternate", - help="Alignment of tab; " - "'left'/'right'/'centre' sets the starting side of the tabs; " - "'full' will force all label tabs to be full width of the divider; sets --tab_number 1 " - "'left-alternate' will start on the left and then toggle between left and right for the tabs," - " sets --tab_number 2; " - "'right-alternate' will start on the right and then toggle between right and left for the tabs," - " sets --tab_number 2; " - "'left-flip' like left-alternate, but the right will be flipped front/back with tab on left," - " sets --tab_number 2; " - "'right-flip' like right-alternate, but the left will be flipped front/back with tab on right," - " sets --tab_number 2; ", - ) - group_tab.add_argument( - "--tab-number", - type=int, - 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 " - "of the tabs will be evenly spaced until ending on the opposite side. Then the cycle repeats. " - "May be overriden by some options of --tab_side.", - ) - group_tab.add_argument( - "--tab-serpentine", - action="store_true", - help="Affects the order of tabs. When not selected, tabs will progress from the starting side (left/right) " - "to the opposite side (right/left), and then repeat (e.g., left to right, left to right, etc.). " - "When selected, the order is changed to smoothly alternate between the two sides " - "(e.g., left to right, to left, to right, etc.) " - "Only valid if --tab_number > 2.", - ) - group_tab.add_argument( - "--tab-name-align", - choices=NAME_ALIGN_CHOICES + ["center"], - dest="tab_name_align", - default="left", - help="Alignment of text on the tab; " - "The 'edge' option will align the card name to the outside edge of the " - "tab, so that when using tabs on alternating sides, " - "the name is less likely to be hidden by the tab in front " - "(edge will revert to left when tab_side is full since there is no edge in that case).", - ) - group_tab.add_argument( - "--tabwidth", - type=float, - default=4.0, - help="Width in cm of stick-up tab (ignored if --tab_side is 'full' or --tabs_only is used).", - ) - group_tab.add_argument( - "--cost", - action="append", - choices=LOCATION_CHOICES, - help="Where to display the card cost; may be set to " - "'hide' to indicate it should not be displayed, or " - "given multiple times to show it in multiple places. " - "(If not given, will default to 'tab'.)", - ) - group_tab.add_argument( - "--set-icon", - action="append", - choices=LOCATION_CHOICES, - help="Where to display the set icon; may be set to " - "'hide' to indicate it should not be displayed, or " - "given multiple times to show it in multiple places. " - "(If not given, will default to 'tab'.)", - ) - group_tab.add_argument( - "--no-tab-artwork", - action="store_true", - dest="no_tab_artwork", - help="Don't show background artwork on tabs.", - ) - group_tab.add_argument( - "--tab-artwork-opacity", - type=float, - default=1.0, - help="Multiply opacity of tab background art by this value; " - "can be used to make text show up clearer on dark backrounds, " - "particularly on printers that output darker than average", - ) - group_tab.add_argument( - "--tab-artwork-resolution", - type=int, - default=0, - help="Limit the DPI resolution of tab background art. " - "If nonzero, any higher-resolution images will be resized to " - "reduce output file size.", - ) - group_tab.add_argument( - "--use-text-set-icon", - action="store_true", - dest="use_text_set_icon", - help="Use text/letters to represent a card's set instead of the set icon.", - ) - group_tab.add_argument( - "--use-set-icon", - action="store_true", - dest="use_set_icon", - help="Use set icon instead of a card icon. Applies to Promo cards.", - ) - group_tab.add_argument( - "--expansion-reset-tabs", - action="store_true", - dest="expansion_reset_tabs", - help="When set, the tabs are restarted (left/right) at the beginning of each expansion. " - "If not set, the tab pattern will continue from one expansion to the next. ", - ) - - # Expanion Dividers - group_expansion = parser.add_argument_group( - "Expansion Dividers", "Adding separator dividers for each expansion." - ) - group_expansion.add_argument( - "--expansion-dividers", - action="store_true", - dest="expansion_dividers", - help="Add dividers describing each expansion set. " - "A list of cards in the expansion will be shown on the front of the divider.", - ) - group_expansion.add_argument( - "--centre-expansion-dividers", - action="store_true", - dest="centre_expansion_dividers", - help="Centre the tabs on expansion dividers (same width as dividers.)", - ) - group_expansion.add_argument( - "--full-expansion-dividers", - action="store_true", - dest="full_expansion_dividers", - help="Full width expansion dividers.", - ) - group_expansion.add_argument( - "--expansion-dividers-long-name", - action="store_true", - dest="expansion_dividers_long_name", - help="Use the long name with edition information on the expansion divider tab. " - "Without this, the shorter expansion name is used on the expansion divider tab.", - ) - - # Divider Selection - group_select = parser.add_argument_group( - "Divider Selection", "What expansions are used, and grouping of dividers." - ) - group_select.add_argument( - "--expansions", - "--expansion", - nargs="*", - action="append", - dest="expansions", - help="Limit dividers to only the specified expansions. " - "If no limits are set, then the latest expansions are included. " - "Expansion names can also be given in the language specified by " - "the --language parameter. Any expansion with a space in the name must " - "be enclosed in double quotes. This may be called multiple times. " - "Values are not case sensitive. Wildcards may be used: " - "'*' any number of characters, '?' matches any single character, " - "'[seq]' matches any character in seq, and '[!seq]' matches any character not in seq. " - "For example, 'dominion*' will match all expansions that start with 'dominion'. " - "Choices available in all languages include: {}".format( - ", ".join("%s" % x for x in get_expansions()[0]) - ), - ) - group_select.add_argument( - "--fan", - nargs="*", - action="append", - dest="fan", - help="Add dividers from the specified fan made expansions. " - "If this option is not used, no fan expansions will be included. " - "Fan made expansion names can also be given in the language specified by " - "the --language parameter. Any fan expansion with a space in the name must " - "be enclosed in double quotes. This may be called multiple times. " - "Values are not case sensitive. Wildcards may be used: " - "'*' any number of characters, '?' matches any single character, " - "'[seq]' matches any character in seq, and '[!seq]' matches any character not in seq. " - "Choices available in all languages include: {}".format( - ", ".join("%s" % x for x in get_expansions()[1]) - ), - ) - group_select.add_argument( - "--exclude-expansions", - "--exclude-expansion", - nargs="*", - action="append", - metavar="EXCLUDED", - dest="exclude_expansions", - help="Limit dividers to not include the specified expansions. " - "Useful if you want all the expansions, except for one or two. " - "If an expansion is explicitly specified with both '--expansion' and " - "'--exclude-expansion', then '--exclude-expansion' wins, and the " - "expansion is NOT included. Expansion names can also be given in the " - "language specified by the --language parameter. Any expansion with a " - "space in the name must be enclosed in double quotes. This may be " - "called multiple times. Values are not case sensitive. Wildcards may " - "be used. See the help for '--expansion' for details on wildcards. May " - "be the name of an official expansion or fan expansion - see the help " - "for --expansion and --fan for a list of possible names.", - ) - group_select.add_argument( - "--edition", - choices=EDITION_CHOICES, - dest="edition", - help="Editions to include: " - "'1' is for all 1st Editions; " - "'2' is for all 2nd Editions; " - "'upgrade' is for all upgrade cards for each expansion; " - "'removed' is for all removed cards for each expansion; " - "'latest' is for the latest edition for each expansion; " - "'all' is for all editions of expansions, upgrade cards, and removed cards; " - " This can be combined with other options to refine the expansions to include in the output." - " (default: all)", - ) - group_select.add_argument( - "--upgrade-with-expansion", - action="store_true", - dest="upgrade_with_expansion", - help="Include any new edition upgrade cards with the expansion being upgraded.", - ) - group_select.add_argument( - "--base-cards-with-expansion", - action="store_true", - help="Print the base cards as part of the expansion (i.e., a divider for 'Silver' " - "will be printed as both a 'Dominion' card and as an 'Intrigue 1st Edition' card). " - "If this option is not given, all base cards are placed in their own 'Base' expansion.", - ) - group_select.add_argument( - "--group-special", - "--special-card-groups", - action="store_true", - dest="group_special", - help="Group cards that generally are used together " - "(e.g., Shelters, Tournament and Prizes, Urchin/Mercenary, etc.).", - ) - group_select.add_argument( - "--group-kingdom", - action="store_true", - dest="group_kingdom", - help="Group cards that have randomizers into the expansion, " - "and those that don't have randomizers into the expansion's 'Extra' section.", - ) - group_select.add_argument( - "--group-global", - nargs="*", - action="append", - dest="group_global", - help="Group all cards of the specified types across all expansions into one 'Extras' divider. " - "This may be called multiple times. Values are not case sensitive. " - "Choices available include: {}".format( - ", ".join("%s" % x for x in get_global_groups()[1]) - ), - ) - group_select.add_argument( - "--no-trash", - action="store_true", - dest="no_trash", - help="Exclude Trash from cards.", - ) - group_select.add_argument( - "--curse10", - action="store_true", - dest="curse10", - help="Package Curse cards into groups of ten cards.", - ) - group_select.add_argument( - "--start-decks", - action="store_true", - dest="start_decks", - help="Include four start decks with the Base cards.", - ) - group_select.add_argument( - "--include-blanks", - type=int, - default=0, - help="Number of blank dividers to include.", - ) - group_select.add_argument( - "--exclude-events", - action="store_true", - help="Group all 'Event' cards across all expansions into one divider." - "Same as '--group-global Events'", - ) - group_select.add_argument( - "--exclude-landmarks", - action="store_true", - help="Group all 'Landmark' cards across all expansions into one divider." - "Same as '--group-global landmarks'", - ) - group_select.add_argument( - "--exclude-projects", - action="store_true", - help="Group all 'Project' cards across all expansions into one divider." - "Same as '--group-global projects'", - ) - group_select.add_argument( - "--exclude-ways", - action="store_true", - help="Group all 'Way' cards across all expansions into one divider." - "Same as '--group-global ways'", - ) - group_select.add_argument( - "--exclude-traits", - action="store_true", - help="Group all 'Trait' cards across all expansions into one divider." - "Same as '--group-global traits'", - ) - group_select.add_argument( - "--only-type-any", - "--only-type", - "--type-any", - nargs="*", - action="append", - dest="only_type_any", - help="Limit dividers to only those with the specified types. " - "A divider is kept if ANY of the provided types are associated with the divider. " - "Default is all types are included. " - "Any type with a space in the name must be enclosed in double quotes. " - "Values are not case sensitive. " - "Choices available in all languages include: {}".format( - ", ".join("%s" % x for x in get_types()) - ), - ) - group_select.add_argument( - "--only-type-all", - "--type-all", - nargs="*", - action="append", - dest="only_type_all", - help="Limit dividers to only those with the specified types. " - "A divider is kept if ALL of the provided types are associated with the divider. " - "Any type with a space in the name must be enclosed in double quotes. " - "Values are not case sensitive. " - "Choices available in all languages include: {}".format( - ", ".join("%s" % x for x in get_types()) - ), - ) - - # Divider Sleeves/Wrappers - group_wrapper = parser.add_argument_group( - "Card Sleeves/Wrappers", "Generating dividers that are card sleeves/wrappers." - ) - group_wrapper.add_argument( - "--wrapper", - action="store_true", - dest="wrapper_meta", - help="Draw sleeves (wrappers) instead of dividers for the cards. " - "Same as --head=strap --tail=folder", - ) - group_wrapper.add_argument( - "--pull-tab", - action="store_true", - dest="pull_tab_meta", - help="Draw folding pull tabs instead of dividers for the cards. " - "Same as --head=tab --tail=cover", - ) - group_wrapper.add_argument( - "--tent", - action="store_true", - dest="tent_meta", - help="Draw folding tent covers instead of dividers for the cards. " - "Same as --head=cover --head-facing=back --head-text=back " - "--tail=tab --tail-facing=front", - ) - group_wrapper.add_argument( - "--head", - choices=HEAD_CHOICES, - dest="head", - default="tab", - help="Top tab or wrapper type: " - "'tab' for divider tabs, " - "'strap' for longer folding tabs, " - "'cover' for matchbook-style folding covers, " - "or 'none' to leave the top edge plain. " - "The folding options create a top spine that you can customize " - "with --spine.", - ) - group_wrapper.add_argument( - "--tail", - choices=TAIL_CHOICES, - dest="tail", - default="none", - help="Bottom tab or wrapper type: " - "'tab' for a bottom tab banner, " - "'strap' for a pull tab under the cards, " - "'cover' for a simple back cover, " - "'folder' to create tab folders, " - "or 'none' to leave the bottom edge plain.", - ) - group_wrapper.add_argument( - "--head-facing", - choices=FACE_CHOICES, - dest="head_facing", - default="front", - help="Text orientation for top tabs and wrappers: " - "'front' shows the text upright when flat, " - "'back' shows it upright when folded over.", - ) - group_wrapper.add_argument( - "--tail-facing", - choices=FACE_CHOICES, - dest="tail_facing", - default="back", - help="Text orientation for tail wrappers: " - "'front' shows the text upright when flat, " - "'back' shows it upright when folded under.", - ) - group_wrapper.add_argument( - "--head-text", - choices=TEXT_CHOICES + FACE_CHOICES, - dest="head_text", - default="blank", - help="Text to print on top cover panels: " - "'card' shows the text from the game card, " - "'rules' shows additional rules for the game card, " - "'blank' leaves the panel blank; " - "'front' uses the same setting as --front; " - "'back' uses the same setting as --back.", - ) - group_wrapper.add_argument( - "--tail-text", - choices=TEXT_CHOICES + FACE_CHOICES, - dest="tail_text", - default="back", - help="Text to print on bottom folder panels: " - "'card' shows the text from the game card, " - "'rules' shows additional rules for the game card, " - "'blank' leaves the panel blank; " - "'front' uses the same setting as --front; " - "'back' uses the same setting as --back.", - ) - group_wrapper.add_argument( - "--head-height", - type=float, - default=0.0, - help="Height of the top panel in centimeters " - "(a value of 0 uses tab height or card height as appropriate).", - ) - group_wrapper.add_argument( - "--tail-height", - type=float, - default=0.0, - help="Height of the bottom panel in centimeters " - "(a value of 0 uses tab height or card height as appropriate).", - ) - group_wrapper.add_argument( - "--spine", - choices=SPINE_CHOICES, - dest="spine", - default="name", - help="Text to print on the spine of top covers: " - "'name' prints the card name; " - "'type' prints the card type; " - "'tab' prints tab text and graphics; " - "'blank' leaves the spine blank. " - "This is only valid with folding --head options.", - ) - group_wrapper.add_argument( - "--thickness", - type=float, - default=2.0, - help="Thickness of a stack of 60 cards (Copper) in centimeters. " - "Typically unsleeved cards are 2.0, thin sleeved cards are 2.4, and thick sleeved cards are 3.2. " - "This is only valid with --wrapper or other folding options.", - ) - group_wrapper.add_argument( - "--sleeved-thick", - action="store_true", - dest="sleeved_thick", - help="Same as --size=sleeved --thickness 3.2.", - ) - group_wrapper.add_argument( - "--sleeved-thin", - action="store_true", - dest="sleeved_thin", - help="Same as --size=sleeved --thickness 2.4.", - ) - group_wrapper.add_argument( - "--notch", - action="store_true", - dest="notch", - help="Creates thumb notches opposite to the divider tabs, " - "which can make it easier to remove cards from wrappers or stacks. " - "Equivalent to --notch-length=1.5 --notch-height=0.25", - ) - group_wrapper.add_argument( - "--notch-length", - type=float, - default=0.0, - help="Sets the length of thumb notches in centimeters.", - ) - group_wrapper.add_argument( - "--notch-height", - type=float, - default=0.0, - help="Sets the height of thumb notches in centimeters.", - ) - - # Printing - group_printing = parser.add_argument_group( - "Printing", "Changes how the Dividers are printed." - ) - group_printing.add_argument( - "--minmargin", - dest="minmargin", - default="1x1", - help="Page margin in cm in the form '<%%f>x<%%f>', left/right x top/bottom).", - ) - group_printing.add_argument( - "--cropmarks", - action="store_true", - dest="cropmarks", - help="Print crop marks on both sides, rather than tab outlines on the front side.", - ) - group_printing.add_argument( - "--linewidth", - type=float, - default=0.1, - help="Width of lines for card outlines and crop marks.", - ) - group_printing.add_argument( - "--front-offset", - type=float, - dest="front_offset", - default=0, - help="Front page horizontal offset points to shift to the right. Only needed for some printers.", - ) - group_printing.add_argument( - "--front-offset-height", - type=float, - dest="front_offset_height", - default=0, - help="Front page vertical offset points to shift upward. Only needed for some printers.", - ) - group_printing.add_argument( - "--back-offset", - type=float, - dest="back_offset", - default=0, - help="Back page horizontal offset points to shift to the right. Only needed for some printers.", - ) - group_printing.add_argument( - "--back-offset-height", - type=float, - dest="back_offset_height", - default=0, - help="Back page vertical offset points to shift upward. Only needed for some printers.", - ) - group_printing.add_argument( - "--vertical-gap", - type=float, - default=0.0, - help="Vertical gap between dividers in centimeters.", - ) - group_printing.add_argument( - "--horizontal-gap", - type=float, - default=0.0, - help="Horizontal gap between dividers in centimeters.", - ) - group_printing.add_argument( - "--no-page-footer", - action="store_true", - dest="no_page_footer", - help="Do not print the expansion name at the bottom of the page.", - ) - group_printing.add_argument( - "--num-pages", - type=int, - default=-1, - help="Stop generating dividers after this many pages, -1 for all.", - ) - group_printing.add_argument( - "--tabs-only", - action="store_true", - dest="tabs_only", - help="Draw only the divider tabs and no divider outlines. " - "Used to print the divider tabs on labels.", - ) - group_printing.add_argument( - "--black-tabs", - action="store_true", - help="In tabs-only mode, draw tabs on black background", - ) - group_printing.add_argument( - "--linetype", - choices=LINE_CHOICES, - dest="linetype", - default="line", - help="The divider outline type. " - "'line' will print a solid line outlining the divider; " - "'dot' will print a dot at each corner of the divider; " - "'cropmarks' will print cropmarks for the divider; " - "'line-cropmarks' will combine 'line' and 'cropmarks'; " - "'dot-cropmarks' will combine 'dot' and 'cropmarks'", - ) - group_printing.add_argument( - "--cropmarkLength", - type=float, - default=0.2, - help="Length of actual drawn cropmark in centimeters.", - ) - group_printing.add_argument( - "--cropmarkSpacing", - type=float, - default=0.1, - help="Spacing between card and the start of the cropmark in centimeters.", - ) - group_printing.add_argument( - "--rotate", - type=int, - choices=[0, 90, 180, 270], - default=0, - help="Divider degrees of rotation relative to the page edge. " - "No optimization will be done on the number of dividers per page.", - ) - group_printing.add_argument( - "--label", - dest="label_name", - choices=get_label_data()[3], - default=None, - help="Use preset label dimentions. Specify a label name. " - "This will override settings that conflict with the preset label settings.", - ) - group_printing.add_argument( - "--info", - action="store_true", - dest="info", - help="Add a page that has all the options used for the file.", - ) - group_printing.add_argument( - "--info-all", - action="store_true", - dest="info_all", - help="Same as --info, but includes pages with all the possible options that can be used.", - ) - group_printing.add_argument( - "--preview", - action="store_true", - help="Only generate a preview png image of the first page", - ) - group_printing.add_argument( - "--preview-resolution", - type=int, - default=150, - help="resolution in DPI to render preview at, for --preview option", - ) - # Special processing - group_special = parser.add_argument_group( - "Miscellaneous", "These options are generally not used." - ) - group_special.add_argument( - "--cardlist", - dest="cardlist", - help="Path to file that enumerates each card on its own line to be included or excluded." - " To include a card, add its card name on a line. The name can optionally be preceeded by '+'." - " To exclude a card, add its card name on a line preseeded by a '-'" - " If any card is included by this method, only cards specified in this file will be printed.", - ) - group_special.add_argument( - "-c", - is_config_file=True, - help="Use the specified configuration file to provide options. " - "Command line options override options from this file.", - ) - group_special.add_argument( - "-w", - 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 - options.help = parser.format_help() if options.info_all else None - return options - - -def clean_opts(options): - if "center" in options.tab_side: - options.tab_side = str(options.tab_side).replace("center", "centre") - - if "center" in options.tab_name_align: - options.tab_name_align = str(options.tab_name_align).replace("center", "centre") - - 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. - 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: - 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: - options.tab_number = 1 # Full is 1 big tab - - if "-alternate" in options.tab_side: - if options.tab_number != 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: - 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 - ) - options.flip = True - else: - options.flip = False - - if options.tab_number < 3 and options.tab_serpentine: - logger.warning("--tab-serpentine only valid if --tab-number > 2.") - options.tab_serpentine = False - - if options.cost is None: - options.cost = ["tab"] - - if options.set_icon is None: - options.set_icon = ["tab"] - - if options.sleeved_thick: - options.thickness = 3.2 - options.sleeved = True - - if options.sleeved_thin: - options.thickness = 2.4 - options.sleeved = True - - # if notch is enabled with missing dimensions, provide defaults - notch = options.notch or options.notch_length or options.notch_height - if notch and not options.notch_length: - options.notch_length = 1.5 - if notch and not options.notch_height: - options.notch_height = 0.25 - - if options.cropmarks and options.linetype == "line": - options.linetype = "cropmarks" - - if "cropmarks" in options.linetype: - options.cropmarks = True - - if options.expansions is None: - # No instance given, so default to the latest Official expansions - options.expansions = ["*"] - if options.edition is None: - options.edition = "latest" - else: - # options.expansions is a list of lists. Reduce to single lowercase list - options.expansions = [ - item.lower() for sublist in options.expansions for item in sublist - ] - if "none" in options.expansions: - # keyword to indicate no options. Same as --expansions without any expansions given. - options.expansions = [] - - if options.exclude_expansions: - # options.exclude_expansions is a list of lists. Reduce to single lowercase list - options.exclude_expansions = [ - item.lower() for sublist in options.exclude_expansions for item in sublist - ] - - if options.edition is None: - # set the default - options.edition = "all" - - if options.fan is None: - # No instance given, so default to no Fan expansions - options.fan = [] - else: - # options.fan is a list of lists. Reduce to single lowercase list - options.fan = [item.lower() for sublist in options.fan for item in sublist] - if "none" in options.fan: - # keyword to indicate no options. Same as --fan without any expansions given - options.fan = [] - - if options.only_type_any is None: - # No instance given, so default to empty list - options.only_type_any = [] - else: - # options.only_type_any is a list of lists. Reduce to single lowercase list - options.only_type_any = list( - set([item.lower() for sublist in options.only_type_any for item in sublist]) - ) - - if options.only_type_all is None: - # No instance given, so default to empty list - options.only_type_all = [] - else: - # options.only_type_any is a list of lists. Reduce to single lowercase list - options.only_type_all = list( - set([item.lower() for sublist in options.only_type_all for item in sublist]) - ) - - if options.group_global is None: - options.group_global = [] - elif not any(options.group_global): - # option given with nothing indicates all possible global groupings - options.group_global = get_global_groups()[1] - else: - # options.group_global is a list of lists. Reduce to single lowercase list - options.group_global = [ - item.lower() for sublist in options.group_global for item in sublist - ] - # For backwards compatibility - if options.exclude_events: - options.group_global.append("events") - if options.exclude_landmarks: - options.group_global.append("landmarks") - if options.exclude_projects: - options.group_global.append("projects") - if options.exclude_ways: - options.group_global.append("ways") - if options.exclude_traits: - options.group_global.append("traits") - # Remove duplicates from the list - options.group_global = list(set(options.group_global)) - - if options.tabs_only and options.label_name is None: - # default is Avery 8867 - options.label_name = "8867" - - options.label = None - if options.label_name is not None: - for label in get_label_data()[0]: - if options.label_name.upper() in [n.upper() for n in label["names"]]: - options.label = label - break - - assert options.label is not None, "Label '{}' not defined".format( - options.label_name - ) - - # Defaults for missing values - label = options.label - label["paper"] = label["paper"] if "paper" in label else "LETTER" - label["tab-only"] = label["tab-only"] if "tab-only" in label else True - label["tab-height"] = ( - label["tab-height"] if "tab-height" in label else label["height"] - ) - label["body-height"] = ( - label["body-height"] - if "body-height" in label - else label["height"] - label["tab-height"] - ) - label["gap-vertical"] = ( - label["gap-vertical"] if "gap-vertical" in label else 0.0 - ) - label["gap-horizontal"] = ( - label["gap-horizontal"] if "gap-horizontal" in label else 0.0 - ) - label["pad-vertical"] = ( - label["pad-vertical"] if "pad-vertical" in label else 0.1 - ) - label["pad-horizontal"] = ( - label["pad-horizontal"] if "pad-horizontal" in label else 0.1 - ) - - # Option Overrides when using labels - MIN_BODY_CM_FOR_COUNT = 0.6 - MIN_BODY_CM_FOR_TEXT = 4.0 - MIN_HEIGHT_CM_FOR_VERTICAL = 5.0 - MIN_WIDTH_CM_FOR_FULL = 5.0 - - options.linewidth = 0.0 - options.cropmarks = False - options.head = "tab" - options.tail = "none" - options.wrapper_meta = options.pull_tab_meta = options.tent_meta = False - options.papersize = label["paper"] - if label["tab-only"]: - options.tabs_only = True - if label["body-height"] < MIN_BODY_CM_FOR_TEXT: - # Not enough room for any text - options.text_front = "blank" - options.text_back = "blank" - if label["body-height"] < MIN_BODY_CM_FOR_COUNT: - # Not enough room for count and type - options.count = False - options.types = False - if label["height"] < MIN_HEIGHT_CM_FOR_VERTICAL: - # Not enough room to make vertical - options.orientation = "horizontal" - if ( - options.label["width"] - 2 * options.label["pad-horizontal"] - ) < MIN_WIDTH_CM_FOR_FULL: - options.tab_side = "full" - options.label = label - - if options.wrapper_meta: - # Same as --head=strap --tail=folder - options.head = "strap" - options.tail = "folder" - if options.pull_tab_meta: - # Same as --head=tab --tail=cover - options.head = "tab" - options.tail = "cover" - if options.tent_meta: - # Same as --head=cover --head-facing=back --head-text=back - # --tail=tab --tail-facing=front - options.head = "cover" - options.head_facing = "back" - options.head_text = "back" - options.tail = "tab" - options.tail_facing = "front" - # Flags set if there's a head wrapper, a tail wrapper, or either - options.headWrapper = options.head in ["strap", "cover", "folder"] - options.tailWrapper = options.tail in ["strap", "cover", "folder"] - options.wrapper = options.headWrapper or options.tailWrapper - - # Expand --head-text and --tail-text if they refer to --front or --back - if options.head_text == "front": - options.head_text = options.text_front - elif options.head_text == "back": - options.head_text = options.text_back - if options.tail_text == "front": - options.tail_text = options.text_front - elif options.tail_text == "back": - options.tail_text = options.text_back - - return options - - -def parseDimensions(dimensionsStr): - x, y = dimensionsStr.upper().split("X", 1) - return (float(x) * cm, float(y) * cm) - def generate_sample(options): from io import BytesIO @@ -1233,243 +36,6 @@ def generate_sample(options): return sample_out.getvalue() -def parse_papersize(spec): - papersize = None - if not spec: - if os.path.exists("/etc/papersize"): - papersize = open("/etc/papersize").readline().upper() - else: - papersize = "LETTER" - else: - papersize = spec.upper() - - try: - paperwidth, paperheight = getattr(pagesizes, papersize) - except AttributeError: - try: - paperwidth, paperheight = parseDimensions(papersize) - logger.info( - ( - f"Using custom paper size, {paperwidth / cm:.2f}cm x {paperheight / cm:.2f}cm" - ) - ) - except ValueError: - paperwidth, paperheight = pagesizes.LETTER - return paperwidth, paperheight - - -def parse_cardsize(spec, sleeved): - spec = spec.upper() - if spec == "SLEEVED" or sleeved: - dominionCardWidth, dominionCardHeight = (9.4 * cm, 6.15 * cm) - logger.info( - ( - 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) - logger.info( - ( - f"Using normal card size, {dominionCardWidth / cm:.2f}cm x{dominionCardHeight / cm:.2f}cm" - ) - ) - else: - dominionCardWidth, dominionCardHeight = parseDimensions(spec) - logger.info( - ( - f"Using custom card size, {dominionCardWidth / cm:.2f}cm x {dominionCardHeight / cm:.2f}cm" - ) - ) - return dominionCardWidth, dominionCardHeight - - -def find_index_of_object(lst=None, attributes=None): - if lst is None: - lst = [] - if attributes is None: - attributes = {} - # Returns the index of the first object in lst that matches the given attributes. Otherwise returns None. - # attributes is a dict of key: value pairs. Object attributes that are lists are checked to have value in them. - for i, d in enumerate(lst): - # Set match to false just in case there are no attributes. - match = False - for key, value in attributes.items(): - # if anything does not match, then break out and start the next one. - match = hasattr(d, key) - if match: - test = getattr(d, key, None) - if type(test) is list: - match = value in test - else: - match = value == test - if not match: - break - - if match: - # If all the attributes are found, then we have a match - return i - - # nothing matched - return None - - -def read_card_data(options): - # Read in the card types - types_db_filepath = os.path.join("card_db", "types_db.json.gz") - with get_resource_stream(types_db_filepath) as typefile: - Card.types = json.loads( - typefile.read().decode("utf-8"), object_hook=CardType.decode_json - ) - assert Card.types, "Could not load any card types from database" - - # extract unique types - type_list = [] - for c in Card.types: - type_list = list(set(c.getTypeNames()) | set(type_list)) - # set up the basic type translation. The actual language will be added later. - Card.type_names = {} - for t in type_list: - Card.type_names[t] = t - - # turn Card.types into a dictionary for later - Card.types = dict(((c.getTypeNames(), c) for c in Card.types)) - - # Read in the card database - card_db_filepath = os.path.join("card_db", "cards_db.json.gz") - with get_resource_stream(card_db_filepath) as cardfile: - cards = json.loads( - cardfile.read().decode("utf-8"), object_hook=Card.decode_json - ) - assert cards, "Could not load any cards from database" - - set_db_filepath = os.path.join("card_db", "sets_db.json.gz") - with get_resource_stream(set_db_filepath) as setfile: - Card.sets = json.loads(setfile.read().decode("utf-8")) - assert Card.sets, "Could not load any sets from database" - new_sets = {} - for s in Card.sets: - # Make sure these are set either True or False - Card.sets[s]["no_randomizer"] = Card.sets[s].get("no_randomizer", False) - Card.sets[s]["fan"] = Card.sets[s].get("fan", False) - Card.sets[s]["has_extras"] = Card.sets[s].get("has_extras", True) - Card.sets[s]["upgrades"] = Card.sets[s].get("upgrades", None) - new_sets[s] = Card.sets[s] - # Make an "Extras" set for normal expansions - if Card.sets[s]["has_extras"]: - e = s + EXPANSION_EXTRA_POSTFIX - new_sets[e] = copy.deepcopy(Card.sets[s]) - new_sets[e]["set_name"] = "*" + s + EXPANSION_EXTRA_POSTFIX + "*" - new_sets[e]["no_randomizer"] = True - new_sets[e]["has_extras"] = False - Card.sets = new_sets - - # Remove the Trash card. Do early before propagating to various sets. - if options.no_trash: - i = find_index_of_object(cards, {"card_tag": "Trash"}) - if i is not None: - del cards[i] - - # Repackage Curse cards into 10 per divider. Do early before propagating to various sets. - if options.curse10: - i = find_index_of_object(cards, {"card_tag": "Curse"}) - if i is not None: - new_cards = [] - cards_remaining = cards[i].getCardCount() - while cards_remaining > 10: - # make a new copy of the card and set count to 10 - new_card = copy.deepcopy(cards[i]) - new_card.setCardCount(10) - new_cards.append(new_card) - cards_remaining -= 10 - - # Adjust original Curse card to the remaining cards (should be 10) - cards[i].setCardCount(cards_remaining) - # Add the new dividers - cards.extend(new_cards) - - # Add any blank cards - if options.include_blanks > 0: - for _ in range(0, options.include_blanks): - c = Card( - card_tag="Blank", - cardset=EXPANSION_GLOBAL_GROUP, - cardset_tag=EXPANSION_GLOBAL_GROUP, - cardset_tags=[EXPANSION_GLOBAL_GROUP], - randomizer=False, - types=("Blank",), - ) - cards.append(c) - - # Create Start Deck dividers. 4 sets. Adjust totals for other cards, too. - # Do early before propagating to various sets. - # The card database contains one prototype divider that needs to be either duplicated or deleted. - if options.start_decks: - # Find the index to the individual cards that need changed in the cards list - StartDeck_index = find_index_of_object(cards, {"card_tag": "Start Deck"}) - Copper_index = find_index_of_object(cards, {"card_tag": "Copper"}) - 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 - logger.warning("Cannot create Start Decks") - - # Remove the Start Deck prototype if we can - if StartDeck_index is not None: - del cards[StartDeck_index] - else: - # Start Deck Constants - STARTDECK_COPPERS = 7 - STARTDECK_ESTATES = 3 - STARTDECK_NUMBER = 4 - - # Add correct card counts to Start Deck prototype. This will be used to make copies. - cards[StartDeck_index].setCardCount(STARTDECK_COPPERS) - cards[StartDeck_index].addCardCount([int(STARTDECK_ESTATES)]) - - # Make new Start Deck Dividers and adjust the corresponding card counts - for x in range(0, STARTDECK_NUMBER): - # Add extra copies of the Start Deck prototype. - # But don't need to add the first one again, since the prototype is already there. - if x > 0: - cards.append(copy.deepcopy(cards[StartDeck_index])) - # Note: By appending, it should not change any of the index values being used - - # Remove Copper and Estate card counts from their dividers - cards[Copper_index].setCardCount( - cards[Copper_index].getCardCount() - STARTDECK_COPPERS - ) - cards[Estate_index].setCardCount( - cards[Estate_index].getCardCount() - STARTDECK_ESTATES - ) - else: - # Remove Start Deck prototype. It is not needed. - StartDeck_index = find_index_of_object(cards, {"card_tag": "Start Deck"}) - if StartDeck_index is not None: - del cards[StartDeck_index] - - # Set cardset_tag and expand cards that are used in multiple sets - new_cards = [] - for card in cards: - sets = list(card.cardset_tags) - if len(sets) > 0: - # Set and save the first one - card.cardset_tag = sets.pop(0) - new_cards.append(card) - for s in sets: - # for the rest, create a copy of the first - if s: - new_card = copy.deepcopy(card) - new_card.cardset_tag = s - new_cards.append(new_card) - cards = new_cards - - # Make sure each card has the right image file. - for card in cards: - card.image = card.setImage() - - return cards - - class CardSorter(object): def __init__(self, order, lang, baseCards): self.order = order @@ -1583,7 +149,7 @@ def add_card_text(cards, language="en_us"): card_text_filepath = os.path.join( "card_db", language, "cards_" + language.lower() + ".json.gz" ) - with get_resource_stream(card_text_filepath) as card_text_file: + with db.get_resource_stream(card_text_filepath) as card_text_file: card_text = json.loads(card_text_file.read().decode("utf-8")) assert language, "Could not load card text for %r" % language @@ -1603,7 +169,7 @@ def add_set_text(options, sets, language="en_us"): language = language.lower() # Read in the set text and store for later set_text_filepath = os.path.join("card_db", language, f"sets_{language}.json.gz") - with get_resource_stream(set_text_filepath) as set_text_file: + with db.get_resource_stream(set_text_filepath) as set_text_file: set_text = json.loads(set_text_file.read().decode("utf-8")) assert set_text, "Could not load set text for %r" % language @@ -1621,7 +187,7 @@ def add_type_text(types=None, language="en_us"): language = language.lower() # Read in the type text and store for later type_text_filepath = os.path.join("card_db", language, f"types_{language}.json.gz") - with get_resource_stream(type_text_filepath) as type_text_file: + with db.get_resource_stream(type_text_filepath) as type_text_file: type_text = json.loads(type_text_file.read().decode("utf-8")) assert type_text, "Could not load type text for %r" % language @@ -1645,7 +211,7 @@ def add_bonus_regex(options, language="en_us"): bonus_regex_filepath = os.path.join( "card_db", language, f"bonuses_{language}.json.gz" ) - with get_resource_stream(bonus_regex_filepath) as bonus_regex_file: + with db.get_resource_stream(bonus_regex_filepath) as bonus_regex_file: bonus_regex = json.loads(bonus_regex_file.read().decode("utf-8")) assert bonus_regex, "Could not load bonus keywords for %r" % language @@ -1727,15 +293,15 @@ def filter_sort_cards(cards, options): old_card_type=t, new_type=types_to_group[t], new_card_tag=types_to_group[t].lower(), - new_cardset_tag=EXPANSION_GLOBAL_GROUP, + new_cardset_tag=config_options.EXPANSION_GLOBAL_GROUP, ) if options.expansions: - options.expansions.append(EXPANSION_GLOBAL_GROUP) + options.expansions.append(config_options.EXPANSION_GLOBAL_GROUP) # Take care of any blank cards if options.include_blanks > 0: if options.expansions: - options.expansions.append(EXPANSION_GLOBAL_GROUP) + options.expansions.append(config_options.EXPANSION_GLOBAL_GROUP) # Group all the special cards together if options.group_special: @@ -1796,22 +362,22 @@ def filter_sort_cards(cards, options): group_cards[card.group_tag].potcost = 0 # Get the final type names in the requested language - Card.type_names = add_type_text(Card.type_names, LANGUAGE_DEFAULT) - if options.language != LANGUAGE_DEFAULT: + Card.type_names = add_type_text(Card.type_names, db.LANGUAGE_DEFAULT) + if options.language != db.LANGUAGE_DEFAULT: Card.type_names = add_type_text(Card.type_names, options.language) for card in cards: card.types_name = " - ".join([Card.type_names[t] for t in card.types]) # Get the card bonus keywords in the requested language - bonus = add_bonus_regex(options, LANGUAGE_DEFAULT) + bonus = add_bonus_regex(options, db.LANGUAGE_DEFAULT) Card.addBonusRegex(bonus) - if options.language != LANGUAGE_DEFAULT: + if options.language != db.LANGUAGE_DEFAULT: bonus = add_bonus_regex(options, options.language) Card.addBonusRegex(bonus) # Fix up cardset text. Waited as long as possible. - Card.sets = add_set_text(options, Card.sets, LANGUAGE_DEFAULT) - if options.language != LANGUAGE_DEFAULT: + Card.sets = add_set_text(options, Card.sets, db.LANGUAGE_DEFAULT) + if options.language != db.LANGUAGE_DEFAULT: Card.sets = add_set_text(options, Card.sets, options.language) # Split out Official and Fan set information @@ -1925,7 +491,7 @@ def filter_sort_cards(cards, options): if options.group_kingdom: # Separate non-Kingdom cards (without Randomizer) into new "Extras" set if not c.randomizer and Card.sets[c.cardset_tag]["has_extras"]: - c.cardset_tag += EXPANSION_EXTRA_POSTFIX + c.cardset_tag += db.EXPANSION_EXTRA_POSTFIX # Add the cardset informaiton to the card and add it to the list of cards to use c.cardset = Card.sets[c.cardset_tag].get("set_name", c.cardset_tag) keep_cards.append(c) @@ -2101,12 +667,16 @@ def calculate_layout(options, cards=None): if cards is None: cards = [] # This is in place to allow for test cases to it call directly to get - options = clean_opts(options) - options.dominionCardWidth, options.dominionCardHeight = parse_cardsize( - options.size, options.sleeved + options = config_options.clean_opts(options) + options.dominionCardWidth, options.dominionCardHeight = ( + config_options.parse_cardsize(options.size, options.sleeved) + ) + options.paperwidth, options.paperheight = config_options.parse_papersize( + options.papersize + ) + options.minmarginwidth, options.minmarginheight = config_options.parse_dimensions( + options.minmargin ) - options.paperwidth, options.paperheight = parse_papersize(options.papersize) - options.minmarginwidth, options.minmarginheight = parseDimensions(options.minmargin) dd = DividerDrawer(options) dd.calculatePages(cards) @@ -2114,7 +684,7 @@ def calculate_layout(options, cards=None): def generate(options): - cards = read_card_data(options) + cards = db.read_card_data(options) assert cards, "No cards after reading" cards = filter_sort_cards(cards, options) assert cards, "No cards after filtering/sorting" @@ -2122,7 +692,7 @@ def generate(options): dd = calculate_layout(options, cards) logger.info( - f"Paper dimensions: {options.paperwidth / cm:.2f}cm (w) x {options.paperheight / cm:.2f}cm (h)".format + f"Paper dimensions: {options.paperwidth / cm:.2f}cm (w) x {options.paperheight / cm:.2f}cm (h)" ) logger.info( f"Tab dimensions: {options.dividerWidthReserved / cm:.2f}cm (w) " @@ -2139,11 +709,11 @@ def generate(options): def main(): - options = parse_opts() + options = config_options.parse_opts() logger.remove() logger.add(sys.stderr, level=options.log_level) - options = clean_opts(options) + options = config_options.clean_opts(options) if options.preview: fname = "{}.{}".format(os.path.splitext(options.outfile)[0], "png") open(fname, "wb").write(generate_sample(options).getvalue()) diff --git a/src/domdiv/tools/bgg_release.py b/src/domdiv/tools/bgg_release.py index 33bb0115..4918afa1 100644 --- a/src/domdiv/tools/bgg_release.py +++ b/src/domdiv/tools/bgg_release.py @@ -19,7 +19,7 @@ def run_generator(args, main): - args = args + " --outfile " + prefix + main + postfix + args = f"{args} --font-dir local_fonts --outfile {prefix}{main}{postfix}" args = args.split() fname = args[-1] print(args) diff --git a/tests/carddb_tests.py b/tests/carddb_tests.py index 34ec423d..cc417b01 100644 --- a/tests/carddb_tests.py +++ b/tests/carddb_tests.py @@ -8,7 +8,7 @@ import pytest from domdiv import cards as domdiv_cards -from domdiv import main +from domdiv import config_options, db, main @pytest.fixture @@ -25,9 +25,9 @@ def rmd(): def test_cardread(): num_cards_expected = 958 - options = main.parse_opts([]) + options = config_options.parse_opts([]) options.data_path = "." - cards = main.read_card_data(options) + cards = db.read_card_data(options) assert len(cards) == num_cards_expected valid_cardsets = { "base", @@ -78,12 +78,12 @@ def test_cardread(): assert c.cardset_tag in valid_cardsets # Option modified card count - options = main.parse_opts( + options = config_options.parse_opts( ["--no-trash", "--curse10", "--start-decks", "--include-blanks", "7"] ) - options = main.clean_opts(options) + options = config_options.clean_opts(options) options.data_path = "." - cards = main.read_card_data(options) + cards = db.read_card_data(options) # Total delta cards is +28 from # Trash: -1 * 3 sets = -3 # Curse: +2 * 4 sets = +8 @@ -92,13 +92,13 @@ def test_cardread(): assert len(cards) == num_cards_expected + 28 -@pytest.mark.parametrize("lang", main.get_languages("card_db")) +@pytest.mark.parametrize("lang", db.get_languages("card_db")) def test_languages_db(lang): print("checking " + lang) # for now, just test that they load - options = main.parse_opts(["--language", lang]) + options = config_options.parse_opts(["--language", lang]) options.data_path = "." - cards = main.read_card_data(options) + cards = db.read_card_data(options) assert cards, '"{}" cards did not read properly'.format(lang) cards = main.add_card_text(cards, "en_us") cards = main.add_card_text(cards, lang) @@ -119,7 +119,7 @@ def change_cwd(d): def test_only_type(): - options = main.parse_opts( + options = config_options.parse_opts( [ "--expansions", "base", @@ -134,9 +134,9 @@ def test_only_type(): "action", ] ) - options = main.clean_opts(options) + options = config_options.clean_opts(options) options.data_path = "." - cards = main.read_card_data(options) + cards = db.read_card_data(options) cards = main.filter_sort_cards(cards, options) # Total 8 from # Blank: +5 added in options @@ -153,7 +153,7 @@ def test_expansion(): # works, that we can use either the # cardset tag or name, and that capitalization # doesn't matter - options = main.parse_opts( + options = config_options.parse_opts( [ "--expansion", "advEntUres", @@ -161,9 +161,9 @@ def test_expansion(): "--expansions=intrigue1stEdition", ] ) - options = main.clean_opts(options) + options = config_options.clean_opts(options) options.data_path = "." - cards = main.read_card_data(options) + cards = db.read_card_data(options) cards = main.filter_sort_cards(cards, options) card_sets = set(x.cardset.lower() for x in cards) assert card_sets == { @@ -181,7 +181,7 @@ def test_exclude_expansion(): # works, that we can use either the # cardset tag or name, and that capitalization # doesn't matter - options = main.parse_opts( + options = config_options.parse_opts( [ "--expansions", "adventures", @@ -194,9 +194,9 @@ def test_exclude_expansion(): "dominion 2nd edition", ] ) - options = main.clean_opts(options) + options = config_options.clean_opts(options) options.data_path = "." - cards = main.read_card_data(options) + cards = db.read_card_data(options) cards = main.filter_sort_cards(cards, options) card_sets = set(x.cardset.lower() for x in cards) assert card_sets == { @@ -212,7 +212,7 @@ def test_expansion_description_card_order(): # test that the expansions cards lists cards # in alphabetical order, like they are printed, # and that accents don't matter - options = main.parse_opts( + options = config_options.parse_opts( [ "--expansions", "hinterlands1stEdition", @@ -223,9 +223,9 @@ def test_expansion_description_card_order(): "Expansion", ] ) - options = main.clean_opts(options) + options = config_options.clean_opts(options) options.data_path = "." - cards = main.read_card_data(options) + cards = db.read_card_data(options) cards = main.filter_sort_cards(cards, options) card_names = [c.strip() for c in cards[0].description.split("|")] # The 26 french card names of the Hinterlands expansion should be sorted as if no accent diff --git a/tests/generation_tests.py b/tests/generation_tests.py index 4cfc7a75..55187fda 100644 --- a/tests/generation_tests.py +++ b/tests/generation_tests.py @@ -2,12 +2,12 @@ import pytest -from domdiv import main +from domdiv import config_options, db, main def get_clean_opts(opts): - options = main.parse_opts(opts) - options = main.clean_opts(options) + options = config_options.parse_opts(opts) + options = config_options.clean_opts(options) return options @@ -17,7 +17,7 @@ def test_standard_opts(): main.generate(options) -@pytest.mark.parametrize("lang", main.get_languages("card_db")) +@pytest.mark.parametrize("lang", db.get_languages("card_db")) def test_languages(lang): print("checking " + lang) options = get_clean_opts(["--language={}".format(lang)]) diff --git a/tests/layout_tests.py b/tests/layout_tests.py index 4e50c33e..d74834a3 100644 --- a/tests/layout_tests.py +++ b/tests/layout_tests.py @@ -1,11 +1,11 @@ from reportlab.lib.units import cm -from domdiv import main +from domdiv import config_options, main def test_horizontal(): # should be the default - options = main.parse_opts([]) + options = config_options.parse_opts([]) assert options.orientation == "horizontal" main.calculate_layout(options) assert options.numDividersHorizontal == 2 @@ -16,7 +16,7 @@ def test_horizontal(): def test_vertical(): - options = main.parse_opts(["--orientation", "vertical"]) + options = config_options.parse_opts(["--orientation", "vertical"]) assert options.orientation == "vertical" main.calculate_layout(options) assert options.numDividersHorizontal == 3 @@ -27,7 +27,7 @@ def test_vertical(): def test_sleeved(): - options = main.parse_opts(["--size", "sleeved"]) + options = config_options.parse_opts(["--size", "sleeved"]) main.calculate_layout(options) assert options.dividerWidth == 9.4 * cm assert options.labelHeight == 0.9 * cm @@ -35,44 +35,44 @@ def test_sleeved(): def test_cost(): - options = main.parse_opts([]) - options = main.clean_opts(options) + options = config_options.parse_opts([]) + options = config_options.clean_opts(options) assert options.cost == ["tab"] - options = main.parse_opts(["--cost=tab"]) - options = main.clean_opts(options) + options = config_options.parse_opts(["--cost=tab"]) + options = config_options.clean_opts(options) assert options.cost == ["tab"] - options = main.parse_opts(["--cost=body-top"]) - options = main.clean_opts(options) + options = config_options.parse_opts(["--cost=body-top"]) + options = config_options.clean_opts(options) assert options.cost == ["body-top"] - options = main.parse_opts(["--cost=hide"]) - options = main.clean_opts(options) + options = config_options.parse_opts(["--cost=hide"]) + options = config_options.clean_opts(options) assert options.cost == ["hide"] - options = main.parse_opts(["--cost=tab", "--cost=body-top"]) - options = main.clean_opts(options) + options = config_options.parse_opts(["--cost=tab", "--cost=body-top"]) + options = config_options.clean_opts(options) assert set(options.cost) == {"tab", "body-top"} def test_set_icon(): - options = main.parse_opts([]) - options = main.clean_opts(options) + options = config_options.parse_opts([]) + options = config_options.clean_opts(options) assert options.set_icon == ["tab"] - options = main.parse_opts(["--set-icon=tab"]) - options = main.clean_opts(options) + options = config_options.parse_opts(["--set-icon=tab"]) + options = config_options.clean_opts(options) assert options.set_icon == ["tab"] - options = main.parse_opts(["--set-icon=body-top"]) - options = main.clean_opts(options) + options = config_options.parse_opts(["--set-icon=body-top"]) + options = config_options.clean_opts(options) assert options.set_icon == ["body-top"] - options = main.parse_opts(["--set-icon=hide"]) - options = main.clean_opts(options) + options = config_options.parse_opts(["--set-icon=hide"]) + options = config_options.clean_opts(options) assert options.set_icon == ["hide"] - options = main.parse_opts(["--set-icon=tab", "--set-icon=body-top"]) - options = main.clean_opts(options) + options = config_options.parse_opts(["--set-icon=tab", "--set-icon=body-top"]) + options = config_options.clean_opts(options) assert set(options.set_icon) == {"tab", "body-top"} diff --git a/tests/text_tab_tests.py b/tests/text_tab_tests.py index 8843ac35..59f7205e 100644 --- a/tests/text_tab_tests.py +++ b/tests/text_tab_tests.py @@ -1,4 +1,4 @@ -from domdiv import main +from domdiv import config_options, main #################### @@ -6,7 +6,7 @@ #################### def test_text_tabs_default(): # should be the default - options = main.parse_opts([]) + options = config_options.parse_opts([]) assert options.text_front == "card" assert options.text_back == "rules" assert options.tab_name_align == "left" @@ -21,73 +21,73 @@ def test_text_tabs_default(): def test_text_card_rules(): - options = main.parse_opts(["--front", "card", "--back", "rules"]) + options = config_options.parse_opts(["--front", "card", "--back", "rules"]) assert options.text_front == "card" assert options.text_back == "rules" def test_text_card_blank(): - options = main.parse_opts(["--front", "card", "--back", "blank"]) + options = config_options.parse_opts(["--front", "card", "--back", "blank"]) assert options.text_front == "card" assert options.text_back == "blank" def test_text_card_card(): - options = main.parse_opts(["--front", "card", "--back", "card"]) + options = config_options.parse_opts(["--front", "card", "--back", "card"]) assert options.text_front == "card" assert options.text_back == "card" def test_text_card_none(): - options = main.parse_opts(["--front", "card", "--back", "none"]) + options = config_options.parse_opts(["--front", "card", "--back", "none"]) assert options.text_front == "card" assert options.text_back == "none" def test_text_rules_rules(): - options = main.parse_opts(["--front", "rules", "--back", "rules"]) + options = config_options.parse_opts(["--front", "rules", "--back", "rules"]) assert options.text_front == "rules" assert options.text_back == "rules" def test_text_rules_blank(): - options = main.parse_opts(["--front", "rules", "--back", "blank"]) + options = config_options.parse_opts(["--front", "rules", "--back", "blank"]) assert options.text_front == "rules" assert options.text_back == "blank" def test_text_rules_card(): - options = main.parse_opts(["--front", "rules", "--back", "card"]) + options = config_options.parse_opts(["--front", "rules", "--back", "card"]) assert options.text_front == "rules" assert options.text_back == "card" def test_text_rules_none(): - options = main.parse_opts(["--front", "rules", "--back", "none"]) + options = config_options.parse_opts(["--front", "rules", "--back", "none"]) assert options.text_front == "rules" assert options.text_back == "none" def test_text_blank_rules(): - options = main.parse_opts(["--front", "blank", "--back", "rules"]) + options = config_options.parse_opts(["--front", "blank", "--back", "rules"]) assert options.text_front == "blank" assert options.text_back == "rules" def test_text_blank_blank(): - options = main.parse_opts(["--front", "blank", "--back", "blank"]) + options = config_options.parse_opts(["--front", "blank", "--back", "blank"]) assert options.text_front == "blank" assert options.text_back == "blank" def test_text_blank_card(): - options = main.parse_opts(["--front", "blank", "--back", "card"]) + options = config_options.parse_opts(["--front", "blank", "--back", "card"]) assert options.text_front == "blank" assert options.text_back == "card" def test_text_blank_none(): - options = main.parse_opts(["--front", "blank", "--back", "none"]) + options = config_options.parse_opts(["--front", "blank", "--back", "none"]) assert options.text_front == "blank" assert options.text_back == "none" @@ -99,7 +99,9 @@ def test_text_blank_none(): def test_tab_left_left(): - options = main.parse_opts(["--tab-name-align", "left", "--tab-side", "left"]) + options = config_options.parse_opts( + ["--tab-name-align", "left", "--tab-side", "left"] + ) assert options.tab_name_align == "left" assert options.tab_side == "left" main.calculate_layout(options) @@ -108,7 +110,9 @@ def test_tab_left_left(): def test_tab_left_right(): - options = main.parse_opts(["--tab-name-align", "left", "--tab-side", "right"]) + options = config_options.parse_opts( + ["--tab-name-align", "left", "--tab-side", "right"] + ) assert options.tab_name_align == "left" assert options.tab_side == "right" main.calculate_layout(options) @@ -117,7 +121,7 @@ def test_tab_left_right(): def test_tab_left_leftalt(): - options = main.parse_opts( + options = config_options.parse_opts( ["--tab-name-align", "left", "--tab-side", "left-alternate"] ) assert options.tab_name_align == "left" @@ -128,7 +132,7 @@ def test_tab_left_leftalt(): def test_tab_left_rightalt(): - options = main.parse_opts( + options = config_options.parse_opts( ["--tab-name-align", "left", "--tab-side", "right-alternate"] ) assert options.tab_name_align == "left" @@ -139,7 +143,9 @@ def test_tab_left_rightalt(): def test_tab_left_full(): - options = main.parse_opts(["--tab-name-align", "left", "--tab-side", "full"]) + options = config_options.parse_opts( + ["--tab-name-align", "left", "--tab-side", "full"] + ) assert options.tab_name_align == "left" assert options.tab_side == "full" main.calculate_layout(options) @@ -150,7 +156,9 @@ def test_tab_left_full(): def test_tab_right_left(): - options = main.parse_opts(["--tab-name-align", "right", "--tab-side", "left"]) + options = config_options.parse_opts( + ["--tab-name-align", "right", "--tab-side", "left"] + ) assert options.tab_name_align == "right" assert options.tab_side == "left" main.calculate_layout(options) @@ -159,7 +167,9 @@ def test_tab_right_left(): def test_tab_right_right(): - options = main.parse_opts(["--tab-name-align", "right", "--tab-side", "right"]) + options = config_options.parse_opts( + ["--tab-name-align", "right", "--tab-side", "right"] + ) assert options.tab_name_align == "right" assert options.tab_side == "right" main.calculate_layout(options) @@ -168,7 +178,7 @@ def test_tab_right_right(): def test_tab_right_leftalt(): - options = main.parse_opts( + options = config_options.parse_opts( ["--tab-name-align", "right", "--tab-side", "left-alternate"] ) assert options.tab_name_align == "right" @@ -179,7 +189,7 @@ def test_tab_right_leftalt(): def test_tab_right_rightalt(): - options = main.parse_opts( + options = config_options.parse_opts( ["--tab-name-align", "right", "--tab-side", "right-alternate"] ) assert options.tab_name_align == "right" @@ -190,7 +200,9 @@ def test_tab_right_rightalt(): def test_tab_right_full(): - options = main.parse_opts(["--tab-name-align", "right", "--tab-side", "full"]) + options = config_options.parse_opts( + ["--tab-name-align", "right", "--tab-side", "full"] + ) assert options.tab_name_align == "right" assert options.tab_side == "full" main.calculate_layout(options) @@ -202,7 +214,9 @@ def test_tab_right_full(): def test_tab_edge_left(): - options = main.parse_opts(["--tab-name-align", "edge", "--tab-side", "left"]) + options = config_options.parse_opts( + ["--tab-name-align", "edge", "--tab-side", "left"] + ) assert options.tab_name_align == "edge" assert options.tab_side == "left" main.calculate_layout(options) @@ -211,7 +225,9 @@ def test_tab_edge_left(): def test_tab_edge_right(): - options = main.parse_opts(["--tab-name-align", "edge", "--tab-side", "right"]) + options = config_options.parse_opts( + ["--tab-name-align", "edge", "--tab-side", "right"] + ) assert options.tab_name_align == "edge" assert options.tab_side == "right" main.calculate_layout(options) @@ -220,7 +236,7 @@ def test_tab_edge_right(): def test_tab_edge_leftalt(): - options = main.parse_opts( + options = config_options.parse_opts( ["--tab-name-align", "edge", "--tab-side", "left-alternate"] ) assert options.tab_name_align == "edge" @@ -231,7 +247,7 @@ def test_tab_edge_leftalt(): def test_tab_edge_rightalt(): - options = main.parse_opts( + options = config_options.parse_opts( ["--tab-name-align", "edge", "--tab-side", "right-alternate"] ) assert options.tab_name_align == "edge" @@ -242,10 +258,12 @@ def test_tab_edge_rightalt(): def test_tab_edge_full(): - options = main.parse_opts(["--tab-name-align", "edge", "--tab-side", "full"]) + options = config_options.parse_opts( + ["--tab-name-align", "edge", "--tab-side", "full"] + ) assert options.tab_name_align == "edge" assert options.tab_side == "full" - options = main.clean_opts(options) + options = config_options.clean_opts(options) main.calculate_layout(options) assert options.tab_name_align == "left" # special check for odd condition assert options.tab_side == "full" @@ -254,7 +272,9 @@ def test_tab_edge_full(): def test_tab_centre_left(): - options = main.parse_opts(["--tab-name-align", "centre", "--tab-side", "left"]) + options = config_options.parse_opts( + ["--tab-name-align", "centre", "--tab-side", "left"] + ) assert options.tab_name_align == "centre" assert options.tab_side == "left" main.calculate_layout(options) @@ -263,7 +283,9 @@ def test_tab_centre_left(): def test_tab_centre_right(): - options = main.parse_opts(["--tab-name-align", "centre", "--tab-side", "right"]) + options = config_options.parse_opts( + ["--tab-name-align", "centre", "--tab-side", "right"] + ) assert options.tab_name_align == "centre" assert options.tab_side == "right" main.calculate_layout(options) @@ -272,7 +294,7 @@ def test_tab_centre_right(): def test_tab_centre_leftalt(): - options = main.parse_opts( + options = config_options.parse_opts( ["--tab-name-align", "centre", "--tab-side", "left-alternate"] ) assert options.tab_name_align == "centre" @@ -283,7 +305,7 @@ def test_tab_centre_leftalt(): def test_tab_centre_rightalt(): - options = main.parse_opts( + options = config_options.parse_opts( ["--tab-name-align", "centre", "--tab-side", "right-alternate"] ) assert options.tab_name_align == "centre" @@ -294,7 +316,9 @@ def test_tab_centre_rightalt(): def test_tab_centre_full(): - options = main.parse_opts(["--tab-name-align", "centre", "--tab-side", "full"]) + options = config_options.parse_opts( + ["--tab-name-align", "centre", "--tab-side", "full"] + ) assert options.tab_name_align == "centre" assert options.tab_side == "full" main.calculate_layout(options) @@ -306,10 +330,12 @@ def test_tab_centre_full(): def test_tab_center_left(): - options = main.parse_opts(["--tab-name-align", "center", "--tab-side", "left"]) + options = config_options.parse_opts( + ["--tab-name-align", "center", "--tab-side", "left"] + ) assert options.tab_name_align == "center" assert options.tab_side == "left" - options = main.clean_opts(options) + options = config_options.clean_opts(options) main.calculate_layout(options) assert options.tab_name_align == "centre" # check for change in value assert options.tab_side == "left"