From 7edb63f52196aab3f9b0a6b2c1f1537ed7bba6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Degros?= <57880064+fdegros@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:24:46 +1100 Subject: [PATCH] Fit the mnemonic font size to the column width of each card The mnemonic font size was derived only from the height of each row. On narrow cards (eg. --card credit with --anonymous, which uses the larger "big" mnemonics), the longest cell ("12 advocate": 2 digits, a space and an 8-letter word) was wider than its column, so words ran into the next column or were clipped at the edge of the card. Use the smaller of the height-based size and the size at which 11 monospaced characters fit within 90% of the column width. Cards that already fit (eg. index) are unchanged. On unmodified master, the longest cell overflowed for the credit card (20, 33 and 59 words) and the photo card (33 words); the new test checks that every card size and word count fits. --- slip39/layout/components.py | 22 ++++++++++++++++++++-- slip39/output_test.py | 15 ++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/slip39/layout/components.py b/slip39/layout/components.py index 61f770ae..e35a97b6 100644 --- a/slip39/layout/components.py +++ b/slip39/layout/components.py @@ -213,6 +213,13 @@ def element( self ): Coordinate = namedtuple( 'Coordinate', ('x', 'y') ) +# A SLIP-39 mnemonic cell is eg. "12 advocate": up to MNEM_CHARS (2 digits, a space, an 8-letter word) +# of monospaced font, each MONO_WIDTH of the font size wide; fit within MNEM_FIT of the column width. +MNEM_CHARS = 11 +MONO_WIDTH = .6 +MNEM_FIT = .9 + + def layout_card( card_size: Coordinate, card_margin: int, @@ -314,6 +321,17 @@ def layout_card( assert num_mnemonics in MNEM_ROWS_COLS, \ f"Invalid SLIP-39 mnemonic word count: {num_mnemonics}" rows,cols = MNEM_ROWS_COLS[num_mnemonics] + + def mnem_size( region, size_ratio ): + """The mnemonic font size (points): from the height of each row, unless that is too wide for + the longest "NN word" (MNEM_CHARS mono characters) to fit in the width of each column.""" + return min( + region.h / rows * PT_IN * size_ratio, + region.w / cols * PT_IN * MNEM_FIT / ( MNEM_CHARS * MONO_WIDTH ), + ) + + mnem_fontsize = mnem_size( card_mnemonics, 9/16 ) + mnem_fontsize_big = mnem_size( card_mnemonics_big, 12/16 ) for r in range( rows ): for c in range( cols ): card_mnemonics.add_region_proportional( @@ -325,7 +343,7 @@ def layout_card( y2 = (r+1)/rows, font = 'mono', bold = True, - size_ratio = 9/16, + size = mnem_fontsize, ) ) card_mnemonics_big.add_region_proportional( @@ -337,7 +355,7 @@ def layout_card( y2 = (r+1)/rows, font = 'mono', bold = True, - size_ratio = 12/16, + size = mnem_fontsize_big, ) ) diff --git a/slip39/output_test.py b/slip39/output_test.py index 903c0b4e..68f9065e 100644 --- a/slip39/output_test.py +++ b/slip39/output_test.py @@ -4,7 +4,8 @@ from fpdf import FPDF, FlexTemplate from .layout import Region, Text, Image, Box, Coordinate -from .defaults import MM_IN +from .layout.components import layout_card, MNEM_CHARS, MONO_WIDTH +from .defaults import MM_IN, PT_IN, CARD_SIZES, MNEM_ROWS_COLS def test_Region(): @@ -73,3 +74,15 @@ 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 + + +def test_card_mnemonics_fit(): + """The longest mnemonic cell ("12 advocate") must fit within its column, for every card size.""" + for card_size,((card_h,card_w),card_margin) in CARD_SIZES.items(): + for num_mnemonics in MNEM_ROWS_COLS: + card = layout_card( Coordinate( y=card_h, x=card_w ), card_margin, num_mnemonics ) + for e in card.elements(): + if e['name'].startswith( 'mnem-' ): + cell_pt = ( e['x2'] - e['x1'] ) / MM_IN * PT_IN + assert e['size'] * MONO_WIDTH * MNEM_CHARS <= cell_pt, \ + f"{card_size} card, {num_mnemonics} words: {e['name']} {e['size']:.2f}pt too big for a {cell_pt:.1f}pt column"