diff --git a/slip39/layout/pdf.py b/slip39/layout/pdf.py index d4b63d2..eefff1f 100644 --- a/slip39/layout/pdf.py +++ b/slip39/layout/pdf.py @@ -187,7 +187,7 @@ def produce_pdf( groups: Dict[str,Tuple[int,List[str]]], # SLIP-39 Groups {: (,[,...]) accounts: Sequence[Sequence[Account]], # The crypto account(s); at least 1 of each required using_bip39: bool, # Using BIP-39 wallets Seed generation - anonymous: Optional[bool] = None, # Avoid printing crypto addresses, QR codes + anonymous: bool = ANONYMOUS, # Avoid printing crypto addresses, QR codes card_format: str = CARD, # 'index' or '(,),' paper_format: Any = None, # 'Letter', (x,y) dimensions in mm. orientations: Sequence[str] = None, # available orientations; default portrait, landscape @@ -355,21 +355,28 @@ def produce_pdf( _,f,b = page[-1][-1] - f['card-title'] = \ - b['card-title'] = f"SLIP39 {g_name}({mn_n+1}/{len(g_mnems)}) for: {name}" - f['card-requires'] = requires - if not (ANONYMOUS if anonymous is None else anonymous): - f['card-crypto1']= f"{accounts[0][0].crypto} {accounts[0][0].path}: {accounts[0][0].address}" + # Anonymous cards show only the group name: not the name, the other groups, nor the + # cryptocurrency addresses and QR codes. + title = g_name + if not anonymous: + title = f"SLIP39 {g_name}" + if len( g_mnems ) > 1: + title = f"{title} {mn_n+1}" + if name: + title = f"{title} for {name}" + f['card-requires'] = requires + f['card-crypto1'] = f"{accounts[0][0].crypto} {accounts[0][0].path}: {accounts[0][0].address}" f['card-qr1'] = io.BytesIO( qr_acct[0].to_string(encoding='unicode').encode('UTF-8')) # get_image() - if not (ANONYMOUS if anonymous is None else anonymous) and len( accounts[0] ) > 1: - f['card-crypto2'] = f"{accounts[0][1].crypto} {accounts[0][1].path}: {accounts[0][1].address}" - f['card-qr2'] = io.BytesIO( qr_acct[1].to_string(encoding='unicode').encode('UTF-8')) # get_image() - f[f'card-g{g_n}'] = \ - b[f'card-g{g_n}'] = f"{g_name:7.7}..{mn_n+1}" if len(g_name) > 8 else f"{g_name} {mn_n+1}" + if len( accounts[0] ) > 1: + f['card-crypto2'] = f"{accounts[0][1].crypto} {accounts[0][1].path}: {accounts[0][1].address}" + f['card-qr2'] = io.BytesIO( qr_acct[1].to_string(encoding='unicode').encode('UTF-8')) # get_image() + f[f'card-g{g_n}'] = \ + b[f'card-g{g_n}'] = f"{g_name:7.7}..{mn_n+1}" if len(g_name) > 8 else f"{g_name} {mn_n+1}" + f['card-title'] = b['card-title'] = title if watermark: f['card-watermark'] = watermark for n,m in enumerate_mnemonic( mnem ).items(): - mn_idx = f"mnem-{n}-big" if (ANONYMOUS if anonymous is None else anonymous) else f"mnem-{n}" + mn_idx = f"mnem-{n}-big" if anonymous else f"mnem-{n}" f[mn_idx] = m # Back contains QR code of the card's SLIP-39 Mnemonic @@ -412,7 +419,7 @@ def write_pdfs( group_threshold = None, # int, or 1/2 of groups by default cryptocurrency = None, # sequence of [ 'ETH:', ... ] to produce accounts for edit = None, # Adjust crypto paths according to the provided path edit - anonymous = None, # Produce cryptocurrency addresses and QR codes + anonymous = ANONYMOUS, # Produce cryptocurrency addresses and QR codes card_format = None, # Eg. "credit"; False outputs no SLIP-39 Mnemonic cards to PDF paper_format = None, # Eg. "Letter", "Legal", (x,y) filename = None, # A file name format w/ {path}, etc. formatters diff --git a/slip39/output_test.py b/slip39/output_test.py index 903c0b4..1e5d913 100644 --- a/slip39/output_test.py +++ b/slip39/output_test.py @@ -1,9 +1,10 @@ +import pytest import qrcode from pytest import approx from fpdf import FPDF, FlexTemplate -from .layout import Region, Text, Image, Box, Coordinate +from .layout import Region, Text, Image, Box, Coordinate, write_pdfs from .defaults import MM_IN @@ -73,3 +74,59 @@ def test_Region(): tpl.render( offsetx = card_size.x * MM_IN, offsety = card_size.y * MM_IN ) #pdf.output( "test.pdf" ) # To view results in test.pdf, uncomment + + +@pytest.fixture +def rendered_cards( monkeypatch ): + """Records the texts of every template rendered into a PDF.""" + rendered = [] + render = FlexTemplate.render + + def record( self, *args, **kwds ): + rendered.append( dict( self.texts )) + return render( self, *args, **kwds ) + monkeypatch.setattr( FlexTemplate, 'render', record ) + return rendered + + +def write_cards( tmp_path, **kwds ): + write_pdfs( + names = [ "Alice" ], + master_secret = bytes.fromhex( "ff" * 16 ), + cryptocurrency = [ "BTC" ], + cover_page = False, + filepath = str( tmp_path ), + filename = "cards", + **kwds + ) + + +def test_cards( tmp_path, rendered_cards ): + """Cards carry the name, the requirements of all groups, the address, and the group label.""" + write_cards( tmp_path ) + cards = [ t for t in rendered_cards if 'card-title' in t ] + fronts = [ t for t in cards if 'card-requires' in t ] # the backs only have a title (and QR code) + assert fronts + assert { t['card-title'] for t in cards } == { + "SLIP39 First for Alice", "SLIP39 Second for Alice", + "SLIP39 Fam 1 for Alice", "SLIP39 Fam 2 for Alice", "SLIP39 Fam 3 for Alice", "SLIP39 Fam 4 for Alice", + *( f"SLIP39 Frens {n} for Alice" for n in range( 1, 7 )), + } + for t in fronts: + assert "Recover w/ 2 of 4" in t['card-requires'] + assert 'card-crypto1' in t and 'card-qr1' in t + assert any( k.startswith( 'card-g' ) for k in t ) + + +def test_anonymous_cards( tmp_path, rendered_cards ): + """Anonymous cards (front and back) show just the group name, and the mnemonic.""" + write_cards( tmp_path, anonymous=True ) + cards = [ t for t in rendered_cards if 'card-title' in t ] + assert cards + assert { t['card-title'] for t in cards } == { "First", "Second", "Fam", "Frens" } + for t in cards: + assert not any( k.startswith(( 'card-g', 'card-requires', 'card-crypto', 'card-qr1', 'card-qr2' )) for k in t ), \ + f"Anonymous card shows: {sorted( t )}" + assert "Alice" not in "".join( str( v ) for v in t.values() ) + # The mnemonic words are the (large) ones + assert any( k.endswith( '-big' ) for t in cards for k in t if k.startswith( 'mnem-' ))