2525import warnings
2626
2727from datetime import datetime
28- from collections import namedtuple
28+ from collections import namedtuple , defaultdict
2929from collections .abc import Callable
3030from pathlib import Path
3131from typing import Dict , List , Tuple , Optional , Sequence , Any
3232
3333import qrcode
34+ import qrcode .image .svg
3435import fpdf # FPDF, FlexTemplate, FPDF_FONT_DIR
36+ import fpdf .svg
3537
3638from ..api import Account , cryptopaths_parser , create , enumerate_mnemonic , group_parser
3739from ..util import chunker
@@ -199,19 +201,24 @@ def produce_pdf(
199201 orientations : Sequence [str ] = None , # available orientations; default portrait, landscape
200202 cover_text : Optional [str ] = None , # Any Cover Page text; we'll append BIP-39 if 'using_bip39'
201203 watermark : Optional [str ] = None ,
204+ double_sided : Optional [bool ]= None ,
202205) -> Tuple [Tuple [str ,str ], fpdf .FPDF , Sequence [Sequence [Account ]]]:
203206 """Produces an FPDF containing the specified SLIP-39 Mnemonics group recovery cards.
204207
205208 Returns the required Paper description [<format>,<orientation>], the FPDF containing the
206209 produced cards, and the cryptocurrency accounts from the supplied slip39.Details.
207210
208211 Makes available several fonts.
212+
213+ Produces double-sided PDF by default, containing QR codes for SLIP-39 Mnemonics on the back.
209214 """
210215 if paper_format is None :
211216 paper_format = PAPER
212217 if orientations is None :
213218 orientations = ('portrait' , 'landscape' )
214219
220+ double_sided = True if double_sided is None else bool ( double_sided )
221+
215222 # Deduce the card size. All papers sizes are specified in inches.
216223 try :
217224 (card_h ,card_w ),card_margin = CARD_SIZES [card_format .lower ()]
@@ -249,18 +256,19 @@ def produce_pdf(
249256 # Convert all of the first group's account(s) to an address QR code
250257 assert accounts and accounts [0 ], \
251258 "At least one cryptocurrency account must be supplied"
252- qr = {}
259+ qr_acct = {}
253260 for i ,acct in enumerate ( accounts [0 ] ):
254261 qrc = qrcode .QRCode (
255- version = None ,
256- error_correction = qrcode .constants .ERROR_CORRECT_M ,
257- box_size = 10 ,
258- border = 1
262+ version = None ,
263+ error_correction = qrcode .constants .ERROR_CORRECT_M ,
264+ box_size = 10 ,
265+ border = 1 ,
266+ image_factory = qrcode .image .svg .SvgPathImage ,
259267 )
260268 qrc .add_data ( acct .address )
261269 qrc .make ( fit = True )
262270
263- qr [i ] = qrc .make_image ()
271+ qr_acct [i ] = qrc .make_image ()
264272 if log .isEnabledFor ( logging .INFO ):
265273 f = io .StringIO ()
266274 qrc .print_ascii ( out = f )
@@ -333,31 +341,66 @@ def produce_pdf(
333341 else :
334342 tpl_cover .render ()
335343
344+ if double_sided :
345+ pdf .add_page ()
346+
347+
348+ # Compute the contents of the cards; the keys and their values are the attributes of
349+ # each template. Creates pages of cards (<pos>,<front>,<back>).
350+ page = [] # A sequence of pages [[<card>,..],..]
336351 card_n = 0
337- page_n = None
338352 for g_n ,(g_name ,(g_of ,g_mnems )) in enumerate ( groups .items () ):
339353 for mn_n ,mnem in enumerate ( g_mnems ):
340- p ,(offsetx ,offsety ) = page_xy ( card_n )
341- if p != page_n :
342- pdf .add_page ()
343- page_n = p
354+ p_n ,(p_x ,p_y ) = page_xy ( card_n )
344355 card_n += 1
356+ if p_n == len (page ):
357+ page .append ([])
358+ page [- 1 ].append ( ((p_n ,(p_x ,p_y )), dict (), dict ()) )
359+
360+ _ ,f ,b = page [- 1 ][- 1 ]
345361
346- tpl ['card-title' ] = f"SLIP39 { g_name } ({ mn_n + 1 } /{ len (g_mnems )} ) for: { name } "
347- tpl ['card-requires' ] = requires
348- tpl ['card-crypto1' ] = f"{ accounts [0 ][0 ].crypto } { accounts [0 ][0 ].path } : { accounts [0 ][0 ].address } "
349- tpl ['card-qr1' ] = qr [0 ].get_image ()
362+ f ['card-title' ] = \
363+ b ['card-title' ] = f"SLIP39 { g_name } ({ mn_n + 1 } /{ len (g_mnems )} ) for: { name } "
364+ f ['card-requires' ] = requires
365+ f ['card-crypto1' ] = f"{ accounts [0 ][0 ].crypto } { accounts [0 ][0 ].path } : { accounts [0 ][0 ].address } "
366+ f ['card-qr1' ] = io .BytesIO ( qr_acct [0 ].to_string (encoding = 'unicode' ).encode ('UTF-8' )) # get_image()
350367 if len ( accounts [0 ] ) > 1 :
351- tpl ['card-crypto2' ] = f"{ accounts [0 ][1 ].crypto } { accounts [0 ][1 ].path } : { accounts [0 ][1 ].address } "
352- tpl ['card-qr2' ] = qr [1 ].get_image ()
353- tpl [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 } "
354- tpl [ 'card-link ' ] = 'slip39.com'
368+ f ['card-crypto2' ] = f"{ accounts [0 ][1 ].crypto } { accounts [0 ][1 ].path } : { accounts [0 ][1 ].address } "
369+ f ['card-qr2' ] = io . BytesIO ( qr_acct [1 ].to_string ( encoding = 'unicode' ). encode ( 'UTF-8' )) # get_image()
370+ f [f'card-g{ g_n } ' ] = \
371+ 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 } "
355372 if watermark :
356- tpl ['card-watermark' ] = watermark
373+ f ['card-watermark' ] = watermark
357374 for n ,m in enumerate_mnemonic ( mnem ).items ():
358- tpl [f"mnem-{ n } " ] = m
375+ f [f"mnem-{ n } " ] = m
376+
377+ # Back contains QR code of the card's SLIP-39 Mnemonic
378+ qrc = qrcode .QRCode (
379+ version = None ,
380+ error_correction = qrcode .constants .ERROR_CORRECT_M ,
381+ box_size = 10 ,
382+ border = 1 ,
383+ image_factory = qrcode .image .svg .SvgPathImage
384+ )
385+ qrc .add_data ( mnem )
386+ qrc .make ( fit = True )
387+
388+ b ['card-qr-mnem' ] = io .BytesIO ( qrc .make_image ().to_string (encoding = 'unicode' ).encode ('UTF-8' )) # get_image()
359389
360- tpl .render ( offsetx = offsetx , offsety = offsety )
390+ # Now render the front and (optionally) back cards for each page
391+ for p_n ,p in enumerate ( page ):
392+ pdf .add_page ()
393+ for c_n ,((_ ,(p_x ,p_y )),f ,b ) in enumerate ( p ):
394+ for key ,txt in f .items ():
395+ tpl [key ] = txt
396+ tpl .render ( offsetx = p_x , offsety = p_y )
397+ if not double_sided :
398+ continue
399+ pdf .add_page ()
400+ for c_n ,((_ ,(p_x ,p_y )),f ,b ) in enumerate ( p ):
401+ for key ,txt in b .items ():
402+ tpl [key ] = txt
403+ tpl .render ( offsetx = pdf .epw - p_x - card_dim .x , offsety = p_y )
361404
362405 return (paper_format ,orientation ),pdf ,accounts
363406
@@ -384,6 +427,7 @@ def write_pdfs(
384427 wallet_paper = None , # default Wallets to output on Letter format paper,
385428 cover_page = True , # Produce a cover page (including BIP-39 Mnemonic, if using_bip39)
386429 watermark = None , # Any watermark desired on each output
430+ double_sided = None ,
387431):
388432 """Writes a PDF containing a unique SLIP-39 encoded Seed Entropy for each of the names specified.
389433
@@ -486,6 +530,7 @@ def write_pdfs(
486530 orientations = ('portrait' , ) if wallet_pwd else None ,
487531 cover_text = cover_text ,
488532 watermark = watermark ,
533+ double_sided = double_sided ,
489534 )
490535
491536 now = datetime .now ()
0 commit comments