Skip to content

SplitLayout

src.layouts.SplitLayout

Bases: NormalLayout

Split card layout, introduced in Invasion.

Source code in src\layouts.py
class SplitLayout(NormalLayout):
    """Split card layout, introduced in Invasion."""
    card_class: str = LayoutType.Split

    # Static properties
    is_nyx: bool = False
    is_land: bool = False
    is_basic_land: bool = False
    is_artifact: bool = False
    is_creature: bool = False
    is_legendary: bool = False
    is_companion: bool = False
    is_colorless: bool = False
    toughness: str = ''
    power: str = ''

    def __str__(self):
        return (f"{self.display_name}"
                f"{f' [{self.set}]' if self.set else ''}"
                f"{f' {{{self.collector_number}}}' if self.collector_number else ''}")

    """
    * Core Data
    """

    @cached_property
    def art_file(self) -> list[Path]:
        """list[Path]: Two image files, second is appended during render process."""
        return [self.file['file']]

    @cached_property
    def display_name(self) -> str:
        """Both side names."""
        return f"{self.name[0]} // {self.name[1]}"

    @cached_property
    def card(self) -> list[dict]:
        """Both side objects."""
        return [c for c in self.scryfall.get('card_faces', [])]

    """
    * Colors
    """

    @cached_property
    def color_identity(self) -> list:
        """Color identity is shared by both halves, use raw Scryfall instead of 'card' data."""
        return self.scryfall.get('color_identity', [])

    @cached_property
    def color_indicator(self) -> str:
        """Color indicator is shared by both halves, use raw Scryfall instead of 'card' data."""
        return get_ordered_colors(self.scryfall.get('color_indicator', []))

    """
    * Images
    """

    @cached_property
    def scryfall_scan(self) -> str:
        """Scryfall large image scan, if available."""
        return self.scryfall.get('image_uris', {}).get('large', '')

    """
    * Collector Info
    """

    @cached_property
    def artist(self) -> str:
        """Card artist name, use Scryfall raw data instead of 'card' data."""
        if self.file.get('artist'):
            return self.file['artist']

        # Check for duplicate last names
        artist, count = self.scryfall.get('artist', 'Unknown'), []
        if '&' in artist:
            for w in artist.split(' '):
                if w in count:
                    count.remove(w)
                count.append(w)
            return ' '.join(count)
        return artist

    """
    * Symbols
    """

    @cached_property
    def watermark(self) -> list[Optional[str]]:
        """Name of the card's watermark file that is actually used, if provided."""
        watermarks: list[Optional[str]] = []
        for wm in self.watermark_svg:
            if not wm:
                watermarks.append(None)
            elif wm.stem.upper() == 'WM':
                watermarks.append(wm.parent.stem.lower())
            else:
                watermarks.append(wm.stem.lower())
        return watermarks

    @cached_property
    def watermark_raw(self) -> list[Optional[str]]:
        """Name of the card's watermark from raw Scryfall data, if provided."""
        return [c.get('watermark', '') for c in self.card]

    @cached_property
    def watermark_svg(self) -> list[Optional[Path]]:
        """Path to the watermark SVG file, if provided."""
        def _find_watermark_svg(wm: str) -> Optional[Path]:
            """Try to find a watermark SVG asset, allowing for special cases and set code fallbacks.

            Args:
                wm: Watermark name or set code to look for.

            Returns:
                Path to a watermark SVG file if found, otherwise None.

            Notes:
                - 'set' maps to the symbol collection of the set this card was first printed in.
                - 'symbol' maps to the symbol collection of this card object's set.
            """
            if not wm:
                return
            wm = wm.lower()

            # Special case watermarks
            if wm in ['set', 'symbol']:
                return get_watermark_svg_from_set(
                    self.first_print.get('set', self.set) if wm == 'set' else self.set)

            # Look for normal watermark
            return get_watermark_svg(wm)

        # Find a watermark SVG for each face
        watermarks = []
        for w in self.watermark_raw:
            if CFG.watermark_mode == WatermarkMode.Disabled:
                # Disabled Mode
                watermarks.append(None)
                continue
            elif CFG.watermark_mode == WatermarkMode.Forced:
                # Forced Mode
                watermarks.append(
                    _find_watermark_svg(
                        CFG.watermark_default))
                continue
            else:
                # Automatic Mode
                path = _find_watermark_svg(w)
                if path or CFG.watermark_mode == WatermarkMode.Automatic:
                    watermarks.append(path)
                    continue
                # Fallback Mode
                watermarks.append(
                    _find_watermark_svg(
                        CFG.watermark_default))
        return watermarks

    """
    * Text Info
    """

    @cached_property
    def name(self) -> list[str]:
        """Both side names."""
        if self.is_alt_lang:
            return [c.get('printed_name', c.get('name', '')) for c in self.card]
        return [c.get('name', '') for c in self.card]

    @cached_property
    def name_raw(self) -> str:
        """Sanitized card name to use for rendered image file."""
        return f"{self.name[0]} _ {self.name[1]}"

    @cached_property
    def type_line(self) -> list[str]:
        """Both side type lines."""
        if self.is_alt_lang:
            return [c.get('printed_type_line', c.get('type_line', '')) for c in self.card]
        return [c.get('type_line', '') for c in self.card]

    @cached_property
    def mana_cost(self) -> list[str]:
        """Both side mana costs."""
        return [c.get('mana_cost', '') for c in self.card]

    @cached_property
    def oracle_text(self) -> list[str]:
        """Both side oracle texts."""
        text = []
        for t in [
            c.get('printed_text', c.get('oracle_text', ''))
            if self.is_alt_lang else c.get('oracle_text', '')
            for c in self.card
        ]:
            # Remove Fuse if present in oracle text data
            t = ''.join(t.split('\n')[:-1]) if 'Fuse' in self.keywords else t
            text.append(t)
        return text

    @cached_property
    def flavor_text(self) -> list[str]:
        """Both sides flavor text."""
        return [c.get('flavor_text', '') for c in self.card]

    """
    * Bool Data
    """

    @cached_property
    def is_hybrid(self) -> list[bool]:
        """Both sides hybrid check."""
        return [f['is_hybrid'] for f in self.frame]

    @cached_property
    def is_colorless(self) -> list[bool]:
        """Both sides colorless check."""
        return [f['is_colorless'] for f in self.frame]

    """
    * Frame Details
    """

    @cached_property
    def frame(self) -> list[FrameDetails]:
        """Both sides frame data."""
        return [get_frame_details(c) for c in self.card]

    @cached_property
    def pinlines(self) -> list[str]:
        """Both sides pinlines identity."""
        return [f['pinlines'] for f in self.frame]

    @cached_property
    def twins(self) -> list[str]:
        """Both sides twins identity."""
        return [f['twins'] for f in self.frame]

    @cached_property
    def background(self) -> list[str]:
        """Both sides background identity."""
        return [f['background'] for f in self.frame]

    @cached_property
    def identity(self) -> list[str]:
        """Both sides frame accurate color identity."""
        return [f['identity'] for f in self.frame]

Attributes

art_file: list[Path]

list[Path]: Two image files, second is appended during render process.

artist: str

Card artist name, use Scryfall raw data instead of 'card' data.

background: list[str]

Both sides background identity.

card: list[dict]

Both side objects.

card_count: Optional[int]

int | None: Number of cards within the card's release set. Only required in 'Normal' Collector Mode.

collector_data: str

collector_number: int

collector_number_raw: Optional[str]

str | None: Card number assigned within release set. Raw string representation, allows non-digits.

color_identity: list

Color identity is shared by both halves, use raw Scryfall instead of 'card' data.

color_indicator: str

Color indicator is shared by both halves, use raw Scryfall instead of 'card' data.

creator: str

date: date

display_name: str

Both side names.

file: CardDetails

Dictionary containing parsed art file details.

first_print: dict

Card data fetched from Scryfall representing the first print of this card.

flavor_text: list[str]

Both sides flavor text.

frame: list[FrameDetails]

Both sides frame data.

frame_effects: list[str]

Array of frame effects, e.g. nyxtouched, snow, etc.

identity: list[str]

Both sides frame accurate color identity.

input_name: str

Card name, version provided in art file name.

is_alt_lang: bool

True if language selected isn't English.

is_colorless: list[bool]

Both sides colorless check.

is_emblem: bool

is_front: bool

True if card is front face.

is_hybrid: list[bool]

Both sides hybrid check.

is_miracle: bool

True if card is a 'Miracle' card.

is_promo: bool

True if card is a promotional print.

is_snow: bool

True if card is a 'Snow' card.

is_token: bool

is_vehicle: bool

True if card is a Vehicle.

keywords: list[str]

Array of keyword abilities, e.g. Flying, Haste, etc.

lang: str

Card print language, uppercase enforced, falls back to settings defined value.

mana_cost: list[str]

Both side mana costs.

name: list[str]

Both side names.

name_raw: str

Sanitized card name to use for rendered image file.

nickname: str

Nickname, typically set inside template logic but can be passed in filename.

oracle_text: list[str]

Both side oracle texts.

oracle_text_raw: str

Card rules text, enforced English representation.

other_face: dict

Card data from opposing face if provided.

other_face_frame: Union[FrameDetails, dict]

Calculated frame information of opposing face, if provided.

other_face_left: Optional[str]

Abridged type of the opposing side to display on bottom MDFC bar.

other_face_mana_cost: str

Mana cost of opposing face.

other_face_oracle_text: str

Rules text of opposing face.

other_face_oracle_text_raw: str

Rules text of opposing face.

other_face_power: str

Creature power of opposing face, if provided.

other_face_right: str

Mana cost or mana ability of opposing side, depending on land or nonland.

other_face_toughness: str

Creature toughness of opposing face, if provided.

other_face_twins: str

Name and title box identity of opposing face.

other_face_type_line: str

Type line of opposing face.

other_face_type_line_raw: str

Type line of opposing face, English language enforced.

pinlines: list[str]

Both sides pinlines identity.

promo_types: list[str]

list[str]: Promo types this card matches, e.g. stamped, datestamped, etc.

rarity: str

Card rarity, interprets 'special' rarities based on card data.

rarity_letter: str

First letter of card rarity, uppercase enforced.

rarity_raw: str

Card rarity, doesn't interpret 'special' rarities.

rules_text: str

Utility definition comprised of rules and flavor text as available.

scryfall: dict

Card data fetched from Scryfall.

scryfall_scan: str

Scryfall large image scan, if available.

set: str

Card set code, uppercase enforced, falls back to 'MTG' if missing.

set_data: dict

Set data from the current hexproof.io data file.

set_type: str

subtypes: list[str]

Subtypes represented, e.g. Elf, Human, Goblin, etc.

supertypes: list[str]

Supertypes represented, e.g. Basic, Legendary, Snow, etc.

symbol_code: str

Code used to match a symbol to this card's set. Provided by hexproof.io.

symbol_svg: Optional[Path]

SVG path definition for card's expansion symbol.

template_file: Path

Template PSD file path, replaced before render process.

transform_icon: str

Transform icon if provided, data possibly deprecated in modern practice.

twins: list[str]

Both sides twins identity.

type_line: list[str]

Both side type lines.

type_line_raw: str

Card type line, enforced English representation.

types: list[str]

Main cards types represented, e.g. Sorcery, Instant, Creature, etc.

types_raw: list[str]

List of types extracted from the raw typeline.

watermark: list[Optional[str]]

Name of the card's watermark file that is actually used, if provided.

watermark_basic: Optional[Path]

Optional[Path]: Path to basic land watermark, if card is a Basic Land.

watermark_raw: list[Optional[str]]

Name of the card's watermark from raw Scryfall data, if provided.

watermark_svg: list[Optional[Path]]

Path to the watermark SVG file, if provided.