Skip to content

Commit 5b36810

Browse files
committed
Default to double-sided cards, with SLIP-39 Mnemonic QR code on back.
1 parent 0b74d3d commit 5b36810

File tree

5 files changed

+84
-31
lines changed

5 files changed

+84
-31
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ chacha20poly1305 >=0.0.3
33
click >=8.1.3,<9
44
crypto-licensing >=3.3.2,<4
55
cx_Freeze >=6.12 ; sys_platform == "win32"
6-
fpdf2 >=2.5.7,<3
6+
fpdf2 >=2.7.6,<3
77
hdwallet >=2.2.1,<3
88
mnemonic >=0.2, <1
99
qrcode >=7.3

slip39/layout/components.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ def layout_card(
281281
card_mnemonics = card_bottom.add_region_proportional(
282282
Region( 'card-mnemonics', x1=0, y1=0, x2=13/16, y2=1 )
283283
)
284+
# QR code for Card Mnemonics on back is center/middle of Mnemonics area
285+
card_mnemonics.add_region(
286+
Image( 'card-qr-mnem' )
287+
).square( justify='T' )
284288

285289
# QR codes sqaare, and anchored to top and bottom of card.
286290
card_bottom.add_region_proportional(
@@ -570,12 +574,16 @@ def layout_components(
570574
comp_rows = int(( pdf.eph - page_margin_mm * 2 * min( 1, 1 - bleed )) // comp_dim.y )
571575
comps_pp = comp_rows * comp_cols
572576

577+
# Compute actual page margins to center cards.
578+
page_margin_tb = ( pdf.eph - comp_rows * comp_dim.y ) / 2
579+
page_margin_lr = ( pdf.epw - comp_cols * comp_dim.x ) / 2
580+
573581
def page_xy( num: int ) -> Tuple[int, Coordinate]:
574582
"""Returns the page, and the coordinates within that page of the num'th component"""
575583
page,nth = divmod( num, comps_pp )
576584
page_rows,page_cols = divmod( nth, comp_cols )
577-
offsetx = page_margin_mm + page_cols * comp_dim.x
578-
offsety = page_margin_mm + page_rows * comp_dim.y
585+
offsetx = page_margin_lr + page_cols * comp_dim.x
586+
offsety = page_margin_tb + page_rows * comp_dim.y
579587
log.debug( f"{ordinal(num)} {comp_dim.x:7.5f}mm x {comp_dim.y:7.5f}mm component on page {page}, offset {offsetx:7.5f}mm x {offsety:7.5f}mm" )
580588
return (page, Coordinate( x=offsetx, y=offsety ))
581589

slip39/layout/pdf.py

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import warnings
2626

2727
from datetime import datetime
28-
from collections import namedtuple
28+
from collections import namedtuple, defaultdict
2929
from collections.abc import Callable
3030
from pathlib import Path
3131
from typing import Dict, List, Tuple, Optional, Sequence, Any
3232

3333
import qrcode
34+
import qrcode.image.svg
3435
import fpdf # FPDF, FlexTemplate, FPDF_FONT_DIR
36+
import fpdf.svg
3537

3638
from ..api import Account, cryptopaths_parser, create, enumerate_mnemonic, group_parser
3739
from ..util import chunker
@@ -207,12 +209,16 @@ def produce_pdf(
207209
produced cards, and the cryptocurrency accounts from the supplied slip39.Details.
208210
209211
Makes available several fonts.
212+
213+
Produces double-sided PDF by default, containing QR codes for SLIP-39 Mnemonics on the back.
210214
"""
211215
if paper_format is None:
212216
paper_format = PAPER
213217
if orientations is None:
214218
orientations = ('portrait', 'landscape')
215219

220+
double_sided = True if double_sided is None else bool( double_sided )
221+
216222
# Deduce the card size. All papers sizes are specified in inches.
217223
try:
218224
(card_h,card_w),card_margin = CARD_SIZES[card_format.lower()]
@@ -250,18 +256,19 @@ def produce_pdf(
250256
# Convert all of the first group's account(s) to an address QR code
251257
assert accounts and accounts[0], \
252258
"At least one cryptocurrency account must be supplied"
253-
qr = {}
259+
qr_acct = {}
254260
for i,acct in enumerate( accounts[0] ):
255261
qrc = qrcode.QRCode(
256-
version = None,
257-
error_correction = qrcode.constants.ERROR_CORRECT_M,
258-
box_size = 10,
259-
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,
260267
)
261268
qrc.add_data( acct.address )
262269
qrc.make( fit=True )
263270

264-
qr[i] = qrc.make_image()
271+
qr_acct[i] = qrc.make_image()
265272
if log.isEnabledFor( logging.INFO ):
266273
f = io.StringIO()
267274
qrc.print_ascii( out=f )
@@ -337,35 +344,63 @@ def produce_pdf(
337344
if double_sided:
338345
pdf.add_page()
339346

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>,..],..]
340351
card_n = 0
341-
page_n = None
342352
for g_n,(g_name,(g_of,g_mnems)) in enumerate( groups.items() ):
343353
for mn_n,mnem in enumerate( g_mnems ):
344-
p,(offsetx,offsety) = page_xy( card_n )
345-
if p != page_n:
346-
pdf.add_page()
347-
page_n = p
348-
349-
if double_sided:
350-
pdf.add_page()
351-
354+
p_n,(p_x,p_y) = page_xy( card_n )
352355
card_n += 1
356+
if p_n == len(page):
357+
page.append([])
358+
page[-1].append( ((p_n,(p_x,p_y)), dict(), dict()) )
353359

354-
tpl['card-title'] = f"SLIP39 {g_name}({mn_n+1}/{len(g_mnems)}) for: {name}"
355-
tpl['card-requires'] = requires
356-
tpl['card-crypto1'] = f"{accounts[0][0].crypto} {accounts[0][0].path}: {accounts[0][0].address}"
357-
tpl['card-qr1'] = qr[0].get_image()
360+
_,f,b = page[-1][-1]
361+
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()
358367
if len( accounts[0] ) > 1:
359-
tpl['card-crypto2'] = f"{accounts[0][1].crypto} {accounts[0][1].path}: {accounts[0][1].address}"
360-
tpl['card-qr2'] = qr[1].get_image()
361-
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}"
362-
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}"
363372
if watermark:
364-
tpl['card-watermark'] = watermark
373+
f['card-watermark'] = watermark
365374
for n,m in enumerate_mnemonic( mnem ).items():
366-
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()
367389

368-
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 )
369404

370405
return (paper_format,orientation),pdf,accounts
371406

@@ -392,6 +427,7 @@ def write_pdfs(
392427
wallet_paper = None, # default Wallets to output on Letter format paper,
393428
cover_page = True, # Produce a cover page (including BIP-39 Mnemonic, if using_bip39)
394429
watermark = None, # Any watermark desired on each output
430+
double_sided = None,
395431
):
396432
"""Writes a PDF containing a unique SLIP-39 encoded Seed Entropy for each of the names specified.
397433
@@ -494,6 +530,7 @@ def write_pdfs(
494530
orientations = ('portrait', ) if wallet_pwd else None,
495531
cover_text = cover_text,
496532
watermark = watermark,
533+
double_sided = double_sided,
497534
)
498535

499536
now = datetime.now()

slip39/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ def main( argv=None ):
115115
ap.add_argument( '--watermark',
116116
default=None,
117117
help="Include a watermark on the output SLIP-39 mnemonic cards" )
118+
ap.add_argument( '--double-sided', action='store_true',
119+
default=None,
120+
help="Enable double-sided PDF (default)" )
121+
ap.add_argument( '--no-double-sided', dest="double_sided", action='store_false',
122+
help="Disable double-sided PDF" )
123+
ap.add_argument( '--single-sided', dest="double_sided", action='store_false',
124+
help="Enable single-sided PDF" )
118125
ap.add_argument( 'names', nargs="*",
119126
help="Account names to produce; if --secret Entropy is supplied, only one is allowed.")
120127
args = ap.parse_args( argv )
@@ -216,6 +223,7 @@ def main( argv=None ):
216223
wallet_format = wallet_format,
217224
cover_page = args.cover_page,
218225
watermark = args.watermark,
226+
double_sided = args.double_sided,
219227
)))
220228
except Exception as exc:
221229
log.exception( f"Failed to write PDFs: {exc}" )

slip39/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version_info__ = ( 11, 0, 3 )
1+
__version_info__ = ( 11, 1, 0 )
22
__version__ = '.'.join( map( str, __version_info__ ))

0 commit comments

Comments
 (0)