diff --git a/.idea/misc.xml b/.idea/misc.xml index 1914eb43..683163a6 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/proxyshop/constants.py b/proxyshop/constants.py index 750416bb..b38cd819 100644 --- a/proxyshop/constants.py +++ b/proxyshop/constants.py @@ -262,135 +262,6 @@ "{CHAOS}": "?" } -# Ability words which should be italicised in formatted text -ability_words = [ - "Adamant", - "Addendum", - "Battalion", - "Bloodrush", - "Channel", - "Chroma", - "Cohort", - "Converge", - "Council's dilemma", - "Delirium", - "Domain", - "Eminence", - "Enrage", - "Fateful hour", - "Ferocious", - "Formidable", - "Grandeur", - "Hellbent", - "Heroic", - "Imprint", - "Inspired", - "Join forces", - "Kinship", - "Landfall", - "Lieutenant", - "Metalcraft", - "Morbid", - "Parley", - "Radiance", - "Raid", - "Rally", - "Revolt", - "Spell mastery", - "Strive", - "Sweep", - "Tempting offer", - "Threshold", - "Undergrowth", - "Will of the council", - "Magecraft", - - # AFR ability words - "Antimagic Cone", - "Fear Ray", - "Pack tactics", - "Acid Breath", - "Teleport", - "Lightning Breath", - "Wild Magic Surge", - "Two-Weapon Fighting", - "Archery", - "Bear Form", - "Mage Hand", - "Cure Wounds", - "Dispel Magic", - "Gentle Reprise", - "Beacon of Hope", - "Displacement", - "Drag Below", - "Siege Monster", - "Dark One's Own Luck", - "Climb Over", - "Tie Up", - "Rappel Down", - "Rejuvenation", - "Engulf", - "Dissolve", - "Poison Breath", - "Tragic Backstory", - "Cunning Action", - "Stunning Strike", - "Circle of Death", - "Bardic Inspiration", - "Song of Rest", - "Sneak Attack", - "Tail Spikes", - "Dominate Monster", - "Flurry of Blows", - "Divine Intervention", - "Split", - "Magical Tinkering", - "Keen Senses", - "Grant an Advantage", - "Smash the Chest", - "Pry It Open", - "Fire Breath", - "Cone of Cold", - "Brave the Stench", - "Search the Body", - "Search the Room", - "Bewitching Whispers", - "Whispers of the Grave", - "Animate Walking Statue", - "Trapped!", - "Invoke Duplicity", - "Combat Inspiration", - "Cold Breath", - "Life Drain", - "Fight the Current", - "Find a Crossing", - "Intimidate Them", - "Fend Them Off", - "Smash It", - "Lift the Curse", - "Steal Its Eyes", - "Break Their Chains", - "Interrogate Them", - "Foil Their Scheme", - "Learn Their Secrets", - "Journey On", - "Make Camp", - "Rouse the Party", - "Set Off Traps", - "Form a Party", - "Start a Brawl", - "Make a Retreat", - "Stand and Fight", - "Distract the Guards", - "Hide", - "Charge Them", - "Befriend Them", - "Negative Energy Cone", - - # Midnight Hunt words - "Coven", -] - # Card rarities rarity_common = "common" rarity_uncommon = "uncommon" @@ -494,7 +365,6 @@ def load_values(self): # Cards self.symbols = symbols - self.ability_words = ability_words self.basic_land_names = basic_land_names self.set_symbols = set_symbols self.Faces = Faces diff --git a/proxyshop/core.py b/proxyshop/core.py index 74ae8992..1011ff3d 100644 --- a/proxyshop/core.py +++ b/proxyshop/core.py @@ -34,6 +34,10 @@ "Planar": ["planar"] } +# REGEX Patterns +re_art = re.compile(r'\(+(.*?)\)') +re_set = re.compile(r'\[(.*)\]') +re_cre = re.compile(r'{(.*)}') """ TEMPLATE FUNCTIONS @@ -151,11 +155,6 @@ def retrieve_card_info(filename): fn_split = re.split('|'.join(map(re.escape, sep)), fn) name = fn_split[0] - # Precompile pattern - re_art = re.compile(r'\(+(.*?)\)') - re_set = re.compile(r'\[(.*)\]') - re_cre = re.compile(r'{(.*)}') - # Match pattern artist = re_art.findall(filename) set_code = re_set.findall(filename) @@ -166,7 +165,11 @@ def retrieve_card_info(filename): else: creator = None if artist: artist = artist[0] else: artist = None - if set_code: set_code = set_code[0] + if set_code: + set_code = set_code[0] + if set_code.upper() not in con.set_symbols: + if set_code.upper()[1:] in con.set_symbols: + set_code = set_code[1:] else: set_code = None return { diff --git a/proxyshop/format_text.py b/proxyshop/format_text.py index 09c98457..ed792288 100644 --- a/proxyshop/format_text.py +++ b/proxyshop/format_text.py @@ -505,6 +505,8 @@ def generate_italics(card_text): * Generates italics text array from card text to italicise all text within (parentheses) and all ability words. """ italic_text = [] + + # Find and add reminder text end_index = 0 while True: start_index = card_text.find("(", end_index) @@ -514,9 +516,19 @@ def generate_italics(card_text): italic_text.extend([card_text[start_index:end_index]]) else: break - # Attach all ability words to the italics array - for ability_word in con.ability_words: - italic_text.extend([ability_word + " \u2014"]) # Include em dash + # Find and add ability words + reg = re.compile(r"[•|\w|\-|\s|]*? — ") + for match in reg.findall(card_text): + # Cover boast cards and choose cards that aren't weird AFR cases + if ( + any(s in match for s in ("• ", "Boast")) + and card_text[0:12] != "Choose one —" + ): continue + + # Fix bullet points and carriage return + if "• " in match: match = match.replace("• ", "") + if "\r" in match: match = match.replace("\r", "") + italic_text.extend([match]) return italic_text @@ -571,10 +583,7 @@ def classic_align_right(primedesc, start, end): return primedesc -def scale_text_right_overlap( - layer: ps.LayerKind.TextLayer, - reference: ps.LayerKind.TextLayer -) -> None: +def scale_text_right_overlap(layer, reference) -> None: """ Scales a text layer down (in 0.2 pt increments) until its right bound has a 24 px clearance from a reference layer's left bound. @@ -591,22 +600,27 @@ def scale_text_right_overlap( elif reference.bounds == [0, 0, 0, 0]: return # Can't find UnitValue object in python api - step_size = 0.2 reference_left_bound = reference.bounds[0] layer_left_bound = layer.bounds[0] layer_right_bound = layer.bounds[2] old_size = float(layer.textItem.size) + step, half_step = 0.4, 0.2 # Obtain proper spacing for this document size - spacing = int((app.activeDocument.width / 3264) * 24) + spacing = int((app.activeDocument.width / 3264) * 36) # Guard against the reference's left bound being left of the layer's left bound if reference_left_bound >= layer_left_bound: # Step down the font till it clears the reference while layer_right_bound > (reference_left_bound - spacing): # minimum 24 px gap - layer.textItem.size -= step_size + layer.textItem.size -= step layer_right_bound = layer.bounds[2] + layer.textItem.size += half_step + layer_right_bound = layer.bounds[2] + if layer_right_bound > (reference_left_bound - spacing): + layer.textItem.size -= half_step + # Shift baseline up to keep text centered vertically if old_size > layer.textItem.size: layer.textItem.baselineShift = (old_size * 0.3) - (layer.textItem.size * 0.3) @@ -615,21 +629,63 @@ def scale_text_right_overlap( if contents: reference.textItem.contents = contents -def scale_text_to_fit_reference(layer, reference_layer): +def scale_text_to_fit_reference(layer, ref, spacing: int = None): """ Resize a given text layer's contents (in 0.25 pt increments) until it fits inside a specified reference layer. The resulting text layer will have equal font and lead sizes. + @param layer: Text layer to scale. + @param ref: Reference layer the text should fit inside. + @param spacing: [Optional] Amount of mandatory spacing at the bottom of text layer. """ # Establish base variables, ensure a level of spacing at the margins - if reference_layer is None: return - spacing = int((app.activeDocument.width / 3264) * 64) - ref_height = psd.get_layer_dimensions(reference_layer)['height'] - spacing + if not ref: return + if not spacing: # If no spacing provided, use default + spacing = int((app.activeDocument.width / 3264) * 64) + ref_height = psd.get_layer_dimensions(ref)['height'] - spacing font_size = layer.textItem.size - step_size = 0.25 + step, half_step = 0.4, 0.2 - # step down font and lead sizes by the step size, and update those sizes in the layer + # Step down font and lead sizes by the step size, and update those sizes in the layer + if ref_height > psd.get_text_layer_dimensions(layer)['height']: return while ref_height < psd.get_text_layer_dimensions(layer)['height']: - font_size -= step_size + font_size -= step + layer.textItem.size = font_size + layer.textItem.leading = font_size + + # Take a half step back up, check if still in bounds and adjust back if needed + font_size += half_step + layer.textItem.size = font_size + layer.textItem.leading = font_size + if ref_height < psd.get_text_layer_dimensions(layer)['height']: + font_size -= half_step + layer.textItem.size = font_size + layer.textItem.leading = font_size + + +def scale_text_to_fit_height(layer, height: int): + """ + Resize a given text layer's contents (in 0.25 pt increments) until it fits inside a specified reference layer. + The resulting text layer will have equal font and lead sizes. + @param layer: Text layer to scale. + @param height: Reference height to fit. + """ + # Establish base variables, ensure a level of spacing at the margins + font_size = layer.textItem.size + step, half_step = 0.4, 0.2 + + # Step down font and lead sizes by the step size, and update those sizes in the layer + if height > psd.get_text_layer_dimensions(layer)['height']: return + while height < psd.get_text_layer_dimensions(layer)['height']: + font_size -= step + layer.textItem.size = font_size + layer.textItem.leading = font_size + + # Take a half step back up, check if still in bounds and adjust back if needed + font_size += half_step + layer.textItem.size = font_size + layer.textItem.leading = font_size + if height < psd.get_text_layer_dimensions(layer)['height']: + font_size -= half_step layer.textItem.size = font_size layer.textItem.leading = font_size diff --git a/proxyshop/helpers.py b/proxyshop/helpers.py index 9b245387..3c4acc96 100644 --- a/proxyshop/helpers.py +++ b/proxyshop/helpers.py @@ -19,6 +19,24 @@ app.preferences.typeUnits = ps.Units.Points +""" +SYSTEM FUNCTIONS +""" + + +def check_fonts(fonts: list): + """ + Check if given fonts exist in users Photoshop Application. + @return: Array of missing fonts or None + """ + missing = [] + for f in fonts: + try: assert isinstance(app.fonts.getByName(f).name, str) + except AssertionError: missing.append(f) + if len(missing) == 0: return + else: return missing + + """ UTILITY FUNCTIONS """ @@ -331,12 +349,21 @@ def select_layer_pixels(layer): app.executeAction(cID("setd"), des1, NO_DIALOG) -def align(align_type = "AdCH"): +def align(align_type = "AdCH", layer = None, ref = None): """ Align the currently active layer to current selection, vertically or horizontally. Used with align_vertical() or align_horizontal(). `align_type`: "AdCV" vertical, "AdCH" horizontal """ + # Optionally create a selection based on given reference + if ref: select_layer_pixels(ref) + + # Optionally make a given layer the active layer + if layer: + current = app.activeDocument.activeLayer + app.activeDocument.activeLayer = layer + + # Align the current layer to selection desc = ps.ActionDescriptor() ref = ps.ActionReference() ref.putEnumerated(cID("Lyr "), cID("Ordn"), cID("Trgt")) @@ -344,19 +371,22 @@ def align(align_type = "AdCH"): desc.putEnumerated(cID("Usng"), cID("ADSt"), cID(align_type)) app.executeAction(cID("Algn"), desc, NO_DIALOG) + # Return to previous current if needded + if layer: app.activeDocument.activeLayer = current + -def align_vertical(): +def align_vertical(layer = None, ref = None): """ Align the currently active layer vertically with respect to the current selection. """ - align("AdCV") + align("AdCV", layer, ref) -def align_horizontal(): +def align_horizontal(layer = None, ref = None): """ Align the currently active layer horizontally with respect to the current selection. """ - align("AdCH") + align("AdCH", layer, ref) def frame_layer(layer, reference, anchor=ps.AnchorPosition.TopLeft, smallest=False, align_h=True, align_v=True): diff --git a/proxyshop/layouts.py b/proxyshop/layouts.py index 0a8f4b14..96729198 100644 --- a/proxyshop/layouts.py +++ b/proxyshop/layouts.py @@ -50,7 +50,7 @@ class BaseLayout: """ Superclass, extend to this class at bare minimum for info all cards should have. """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): """ Constructor for base layout that unpacks scryfall data shared by all card types, includes method that calls select_frame_layers to determine frame colors for the card. @@ -97,7 +97,6 @@ def __init__(self, scryfall, card_name): # Prepare set code self.set = self.scryfall['set'].upper() - if len(self.set) > 3: self.set = self.set[1:] # Prepare rarity letter + collector number self.collector_number = self.scryfall['collector_number'] @@ -105,11 +104,15 @@ def __init__(self, scryfall, card_name): self.collector_number = f"0{self.collector_number}" elif len(self.collector_number) == 1: self.collector_number = f"00{self.collector_number}" + self.collector_number = self.collector_number.replace("p", "") # Was card count already provided? if 'printed_size' not in self.scryfall: # Get set info to find card count - self.mtgset = scry.set_info(self.scryfall['set']) + if len(self.set) > 3 and self.set[0] == "P": + self.mtgset = scry.set_info(self.scryfall['set'][1:]) + else: + self.mtgset = scry.set_info(self.scryfall['set']) try: self.card_count = self.mtgset['baseSetSize'] except (KeyError, TypeError): try: self.card_count = self.mtgset['totalSetSize'] @@ -130,6 +133,8 @@ def __init__(self, scryfall, card_name): # Automatic set symbol enabled? if cfg.auto_symbol and self.set in con.set_symbols: self.symbol = con.set_symbols[self.set] + elif cfg.auto_symbol and self.set[1:] in con.set_symbols: + self.symbol = con.set_symbols[self.set[1:]] else: self.symbol = cfg.symbol_char # Optional vars @@ -187,7 +192,7 @@ class NormalLayout (BaseLayout): """ Use this as Superclass for most regular layouts """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): super().__init__(scryfall, card_name) # Mandatory vars @@ -226,7 +231,7 @@ class TransformLayout (BaseLayout): """ Used for transform cards """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): super().__init__(scryfall, card_name) # Mandatory vars @@ -280,7 +285,7 @@ class MeldLayout (NormalLayout): """ Used for Meld cards """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): super().__init__(scryfall, card_name) # Determine if this card is a meld part or a meld result @@ -316,7 +321,7 @@ class ModalDoubleFacedLayout (BaseLayout): """ Used for Modal Double Faced cards """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): super().__init__(scryfall, card_name) # Mandatory vars @@ -403,7 +408,7 @@ class AdventureLayout (BaseLayout): """ Used for Adventure cards """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): super().__init__(scryfall, card_name) # Mandatory vars @@ -444,7 +449,7 @@ class LevelerLayout (NormalLayout): """ Used for Leveler cards """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): super().__init__(scryfall, card_name) # Unpack oracle text into: level text, levels x-y text, levels z+ text, middle level, @@ -472,7 +477,7 @@ class SagaLayout (NormalLayout): """ Used for Saga cards """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): super().__init__(scryfall, card_name) # Unpack oracle text into saga lines @@ -491,7 +496,7 @@ class PlanarLayout (BaseLayout): """ Used for Planar cards """ - def __init__(self, scryfall, card_name): + def __init__(self, scryfall: dict, card_name: str): super().__init__(scryfall, card_name) # Mandatory vars diff --git a/proxyshop/plugins/SilvanMTG/templates.py b/proxyshop/plugins/SilvanMTG/templates.py index b222eb5c..8794bc5a 100644 --- a/proxyshop/plugins/SilvanMTG/templates.py +++ b/proxyshop/plugins/SilvanMTG/templates.py @@ -20,6 +20,13 @@ def __init__(self, layout): cfg.remove_reminder = True super().__init__(layout) + def enable_frame_layers(self): + super().enable_frame_layers() + + # Remove colorless background + if self.layout.background == "Colorless": + psd.getLayer(self.layout.background, con.layers['BACKGROUND']).visible = False + def load_artwork(self): super().load_artwork() diff --git a/proxyshop/templates.py b/proxyshop/templates.py index fb54fb42..c6f41107 100644 --- a/proxyshop/templates.py +++ b/proxyshop/templates.py @@ -513,8 +513,7 @@ def basic_text_layers(self, text_and_icons): flavor = self.layout.flavor_text, centered = is_centered, reference = reference_layer, - divider = psd.getLayer(con.layers['DIVIDER'], text_and_icons), - fix_length = False + divider = psd.getLayer(con.layers['DIVIDER'], text_and_icons) ) ) diff --git a/proxyshop/text_layers.py b/proxyshop/text_layers.py index 69891b1e..b83029b2 100644 --- a/proxyshop/text_layers.py +++ b/proxyshop/text_layers.py @@ -1,6 +1,8 @@ """ TEXT LAYER MODULE """ +import math + import photoshop.api as ps import proxyshop.helpers as psd from proxyshop.constants import con @@ -259,7 +261,7 @@ def get_italics_and_flavor_index(self): if len(flavor_text_split) > 1: # Text between asterisk is present italic_text.extend( - [v for i, v in enumerate(flavor_text_split) if not i % 2] + [v for i, v in enumerate(flavor_text_split) if not i % 2 and not v == ""] ) # reassemble flavor text without asterisks @@ -303,8 +305,7 @@ def __init__( flavor = "", reference = None, divider = None, - centered = False, - fix_length = True + centered = False ): super().__init__(layer, contents, color, flavor, centered) if divider and cfg.flavor_divider and len(self.flavor_text) > 0 and len(self.contents) > 0: @@ -314,10 +315,8 @@ def __init__( self.reference = reference # Prepare for text being too long - if fix_length and len(self.contents+self.flavor_text) > 300: - steps = int((len(self.contents+self.flavor_text)-200)/100) - layer.textItem.size = layer.textItem.size - steps - layer.textItem.leading = layer.textItem.leading - steps + if len(self.contents+self.flavor_text) > 280: self.fix_length = True + else: self.fix_length = False def insert_divider(self): """ @@ -369,6 +368,12 @@ def insert_divider(self): psd.clear_selection() def execute(self): + + # Fix length procedure before super called + if self.fix_length: + self.layer.textItem.contents = self.contents + "\r" + self.flavor_text + ft.scale_text_to_fit_height(self.layer, int(psd.get_layer_dimensions(self.reference)['height']*1.01)) + super().execute() if self.contents != "" or self.flavor_text != "": # Resize the text until it fits into the reference layer @@ -405,10 +410,9 @@ def __init__( divider = None, pt_reference = None, pt_top_reference = None, - centered = False, - fix_length = True + centered = False ): - super().__init__(layer, contents, color, flavor, reference, divider, centered, fix_length) + super().__init__(layer, contents, color, flavor, reference, divider, centered) self.pt_reference = pt_reference self.pt_top_reference = pt_top_reference