Skip to content

Commit 4fdb89f

Browse files
committed
Merge branch 'feature-double-sided'; v11.1.0
2 parents 785004e + 5b36810 commit 4fdb89f

File tree

5 files changed

+88
-27
lines changed

5 files changed

+88
-27
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: 68 additions & 23 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
@@ -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()

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)