#
# Bitcoin-Safe
# Copyright (C) 2023-2026 Andreas Griffin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of version 3 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
from __future__ import annotations
import atexit
import io
import logging
import os
import tempfile
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any
import platformdirs
from bitcoin_qr_tools.qr_generator import QRGenerator
from bitcoin_usb.address_types import DescriptorInfo
from PIL.Image import Image as PilImage
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.platypus import (
Image,
PageBreak,
Paragraph,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
from bitcoin_safe.constants import APP_NAME
from bitcoin_safe.gui.qt.util import svg_tools, svg_tools_hardware_signer
from bitcoin_safe.hardware_signers import HardwareSigner, HardwareSigners
from bitcoin_safe.i18n import translate
from .gui.qt.util import qicon_to_pil
from .util_os import xdg_open_file
from .wallet import Wallet
logger = logging.getLogger(__name__)
_TEMP_PDFS: set[Path] = set()
def _cleanup_temp_pdfs() -> None:
for path in list(_TEMP_PDFS):
try:
path.unlink()
except FileNotFoundError:
pass
except PermissionError:
logger.warning("Could not remove temporary PDF at %s", path)
atexit.register(_cleanup_temp_pdfs)
TEXT_24_WORDS = translate("pdf", "12 or 24")
DEFAULT_MARGIN = 36
def _safe_filename_prefix(filename: str) -> str:
return "".join(char if char.isalnum() or char in {"-", "_"} else "_" for char in filename)
def write_and_open_temp_pdf(pdf: BasePDF, filename: str) -> None:
"""Write a PDF to the user cache directory and open it with the OS."""
safe_prefix = _safe_filename_prefix(filename)
cache_dir = Path(platformdirs.user_cache_dir("bitcoin_safe"))
cache_dir.mkdir(parents=True, exist_ok=True)
temp_fd, temp_file = tempfile.mkstemp(prefix=f"{safe_prefix}-", suffix=".pdf", dir=cache_dir)
os.close(temp_fd)
pdf.save_pdf(temp_file)
pdf.open_pdf(temp_file)
_TEMP_PDFS.add(Path(temp_file))
def pilimage_to_reportlab(pilimage: PilImage, width=200, height=200) -> Image:
"""Pilimage to reportlab."""
buffer = io.BytesIO()
pilimage.save(buffer, format="PNG")
buffer.seek(0)
return Image(buffer, width=width, height=height)
# Define an enum for font types
class FontType(Enum):
CID = "cid"
BUILTIN = "builtin"
@dataclass
class FontInfo:
font_name: str
font_type: FontType
supported_lang_code: str
def register_font(lang_code: str) -> FontInfo:
"""Registers a font for the given language code and returns a FontInfo object that
contains the font details, font type, and supported language.
If the language is not fully supported, it falls back to 'en_US' and uses Helvetica.
:param lang_code: The language code to register the font for (e.g., 'zh_CN', 'ru_RU').
:return: A FontInfo object with the font details and the supported language.
"""
# Mapping language codes to FontInfo instances
FONT_MAP: dict[str, FontInfo] = {
# CID Fonts
"zh_CN": FontInfo("STSong-Light", FontType.CID, "zh_CN"), # Simplified Chinese
"zh_TW": FontInfo("MSung-Light", FontType.CID, "zh_TW"), # Traditional Chinese (Taiwan)
"zh_HK": FontInfo("MHei-Medium", FontType.CID, "zh_HK"), # Traditional Chinese (Hong Kong)
"ja_JP": FontInfo("HeiseiMin-W3", FontType.CID, "ja_JP"), # Japanese
"ko_KR": FontInfo("HYGoThic-Medium", FontType.CID, "ko_KR"), # Korean
# Built-in Fonts (Latin-based)
"es_ES": FontInfo("Helvetica", FontType.BUILTIN, "es_ES"), # Spanish
"fr_FR": FontInfo("Helvetica", FontType.BUILTIN, "fr_FR"), # French
"en_US": FontInfo("Helvetica", FontType.BUILTIN, "en_US"), # English (US)
"en_GB": FontInfo("Helvetica", FontType.BUILTIN, "en_GB"), # English (UK)
"pt_PT": FontInfo("Helvetica", FontType.BUILTIN, "pt_PT"), # Portuguese (Portugal)
"pt_BR": FontInfo("Helvetica", FontType.BUILTIN, "pt_BR"), # Portuguese (Brazil)
"it_IT": FontInfo("Helvetica", FontType.BUILTIN, "it_IT"), # Italian
"de_DE": FontInfo("Helvetica", FontType.BUILTIN, "de_DE"), # German
"nl_NL": FontInfo("Helvetica", FontType.BUILTIN, "nl_NL"), # Dutch (Netherlands)
"nl_BE": FontInfo("Helvetica", FontType.BUILTIN, "nl_BE"), # Dutch (Belgium)
"sv_SE": FontInfo("Helvetica", FontType.BUILTIN, "sv_SE"), # Swedish
"da_DK": FontInfo("Helvetica", FontType.BUILTIN, "da_DK"), # Danish
"no_NO": FontInfo("Helvetica", FontType.BUILTIN, "no_NO"), # Norwegian
"fi_FI": FontInfo("Helvetica", FontType.BUILTIN, "fi_FI"), # Finnish
"is_IS": FontInfo("Helvetica", FontType.BUILTIN, "is_IS"), # Icelandic
"pl_PL": FontInfo("Helvetica", FontType.BUILTIN, "pl_PL"), # Polish
"cs_CZ": FontInfo("Helvetica", FontType.BUILTIN, "cs_CZ"), # Czech
"sk_SK": FontInfo("Helvetica", FontType.BUILTIN, "sk_SK"), # Slovak
"sl_SI": FontInfo("Helvetica", FontType.BUILTIN, "sl_SI"), # Slovenian
"hu_HU": FontInfo("Helvetica", FontType.BUILTIN, "hu_HU"), # Hungarian
"ro_RO": FontInfo("Helvetica", FontType.BUILTIN, "ro_RO"), # Romanian
"hr_HR": FontInfo("Helvetica", FontType.BUILTIN, "hr_HR"), # Croatian
"sr_RS": FontInfo("Helvetica", FontType.BUILTIN, "sr_RS"), # Serbian (Latin)
"bs_BA": FontInfo("Helvetica", FontType.BUILTIN, "bs_BA"), # Bosnian
"mk_MK": FontInfo("Helvetica", FontType.BUILTIN, "mk_MK"), # Macedonian (Latin)
"mt_MT": FontInfo("Helvetica", FontType.BUILTIN, "mt_MT"), # Maltese
"gl_ES": FontInfo("Helvetica", FontType.BUILTIN, "gl_ES"), # Galician
"ca_ES": FontInfo("Helvetica", FontType.BUILTIN, "ca_ES"), # Catalan
"eu_ES": FontInfo("Helvetica", FontType.BUILTIN, "eu_ES"), # Basque
"lv_LV": FontInfo("Helvetica", FontType.BUILTIN, "lv_LV"), # Latvian
"lt_LT": FontInfo("Helvetica", FontType.BUILTIN, "lt_LT"), # Lithuanian
"et_EE": FontInfo("Helvetica", FontType.BUILTIN, "et_EE"), # Estonian
"af_ZA": FontInfo("Helvetica", FontType.BUILTIN, "af_ZA"), # Afrikaans
"vi_VN": FontInfo("Helvetica", FontType.BUILTIN, "vi_VN"), # Vietnamese (Latin-based characters)
"ms_MY": FontInfo("Helvetica", FontType.BUILTIN, "ms_MY"), # Malay
"id_ID": FontInfo("Helvetica", FontType.BUILTIN, "id_ID"), # Indonesian
}
if lang_code in FONT_MAP:
font_info: FontInfo = FONT_MAP[lang_code]
if font_info.font_type == FontType.CID:
# Register the CID font
pdfmetrics.registerFont(UnicodeCIDFont(font_info.font_name))
print(f"Using built-in CID font: {font_info.font_name} for language code: {lang_code}")
elif font_info.font_type == FontType.BUILTIN:
# No registration needed for built-in fonts like Helvetica
print(f"Using built-in font: {font_info.font_name} for language code: {lang_code}")
return font_info
else:
print(f"No font found for language code: {lang_code}, returning en_US")
return FontInfo("Helvetica", FontType.BUILTIN, "en_US") # Default fallback to en_US
white_space = ' - '
class BasePDF:
def __init__(self, lang_code: str) -> None:
"""Initialize instance."""
font_info = register_font(lang_code=lang_code)
self.font_name = font_info.font_name
self.no_translate = font_info.supported_lang_code == "en_US"
self.styles = getSampleStyleSheet()
self.style_paragraph = ParagraphStyle(
name="Centered",
fontName=self.font_name,
parent=self.styles["BodyText"],
alignment=TA_CENTER,
)
self.style_paragraph_left = ParagraphStyle(
name="LEFT",
fontName=self.font_name,
parent=self.styles["BodyText"],
alignment=TA_LEFT,
)
self.style_paragraph_right = ParagraphStyle(
name="LEFT",
fontName=self.font_name,
parent=self.styles["BodyText"],
alignment=TA_RIGHT,
)
self.style_heading = ParagraphStyle(
"centered_heading",
fontName=self.font_name,
parent=self.styles["Heading1"],
alignment=TA_CENTER,
)
self.style_text = ParagraphStyle(
name="normal",
fontName=self.font_name,
)
self.elements: list[Any] = []
def save_pdf(self, filename: str) -> None:
# Adjust these values to set your desired margins (values are in points; 72 points = 1 inch)
"""Save pdf."""
LEFT_MARGIN = 36 # 0.5 inch
RIGHT_MARGIN = 36 # 0.5 inch
TOP_MARGIN = 36 # 0.5 inch
BOTTOM_MARGIN = 36 # 0.5 inch
document = SimpleDocTemplate(
filename,
pagesize=letter,
leftMargin=LEFT_MARGIN,
rightMargin=RIGHT_MARGIN,
topMargin=TOP_MARGIN,
bottomMargin=BOTTOM_MARGIN,
)
document.build(self.elements)
def open_pdf(self, filename: str) -> None:
"""Open pdf."""
if os.path.exists(filename):
xdg_open_file(Path(filename))
else:
logger.info(translate("pdf", "File not found!"))
class DataExportPDF(BasePDF):
def create_pdf(
self,
title: str,
txid: str | None,
serialized: str,
data_label: str,
qr_images: list[PilImage],
ur_qr_images: list[PilImage] | None = None,
) -> None:
"""Create a PDF that captures serialized data and its QR codes."""
self.elements.append(Paragraph(title, style=self.style_heading))
self.elements.append(
Paragraph(
translate("pdf", "Created with", no_translate=self.no_translate)
+ f" {APP_NAME}: {white_space * 5} www.bitcoin-safe.org",
self.style_paragraph,
)
)
self.elements.append(Spacer(1, 12))
if txid:
self.elements.append(
Paragraph(
translate("pdf", "Transaction ID: {txid}", no_translate=self.no_translate).format(
txid=txid
),
self.style_paragraph_left,
)
)
self.elements.append(Spacer(1, 6))
self.elements.append(
Paragraph(
translate("pdf", "Serialized {label}:", no_translate=self.no_translate).format(
label=data_label
),
self.style_paragraph_left,
)
)
self.elements.append(Paragraph(serialized, self.style_paragraph_left))
self.elements.append(Spacer(1, 12))
def _append_qr_section(title: str, images: list[PilImage]):
if not images:
return
self.elements.append(Paragraph(title, self.style_paragraph))
for idx, image in enumerate(images):
self.elements.append(
Paragraph(
translate(
"pdf",
"{title}: Fragment {index} of {total}",
no_translate=self.no_translate,
).format(index=idx + 1, total=len(images), title=title),
self.style_paragraph,
)
)
self.elements.append(pilimage_to_reportlab(image, width=200, height=200))
self.elements.append(Spacer(1, 6))
_append_qr_section(translate("pdf", "BBQr QR", no_translate=self.no_translate), qr_images)
_append_qr_section(translate("pdf", "UR QR", no_translate=self.no_translate), ur_qr_images or [])
if not qr_images and not ur_qr_images:
self.elements.append(
Paragraph(
translate("pdf", "No QR data available", no_translate=self.no_translate),
self.style_paragraph_left,
)
)
class BitcoinWalletRecoveryPDF(BasePDF):
@property
def TEXT_24_WORDS(self):
"""TEXT 24 WORDS."""
return translate("pdf", "12 or 24", no_translate=self.no_translate)
@staticmethod
def create_table(columns: list[Any], col_widths: list[int]) -> Table:
# Validate input and create data for the table
"""Create table."""
max_rows = max([len(col) for col in columns])
data = []
for i in range(max_rows):
row = [col[i] if i < len(col) else "" for col in columns]
data.append(row)
# Create a Table with data and specify column widths
table = Table(data, colWidths=col_widths)
# Apply TableStyle to make the borders invisible
style = TableStyle(
[
("BOX", (0, 0), (-1, -1), 0, colors.white), # Outer border
("INNERGRID", (0, 0), (-1, -1), 0, colors.white), # Inner grid
("VALIGN", (0, 0), (-1, -1), "MIDDLE"), # Vertical alignment for all cells
("ALIGN", (0, 0), (-1, -1), "CENTER"), # Vertical alignment for all cells
# ('VALIGN', (0, 0), (0, 0), 'TOP'), # Vertical alignment for first cell
# ('VALIGN', (1, 0), (1, 0), 'BOTTOM') # Vertical alignment for second cell
]
)
table.setStyle(style)
return table
def add_page_break(self) -> None:
"""Add page break."""
self.elements.append(PageBreak()) # Add a page break between documents if needed
def _seed_part(
self,
seed: str | None,
keystore_description: str,
keystore_fingerprint: str,
keystore_label: str,
num_signers: int,
) -> None:
"""Seed part."""
self.elements.append(Spacer(1, 5))
# Additional subtitle
if num_signers == 1:
instructions1 = Paragraph(
translate(
"pdf",
"""1. Attach the 'Recovery sheet' ({number} words) to this paper or duplicate the 12/24 words onto this paper.
2. Fold this paper at the line below
3. Put this paper in a secure location, where only you have access
4. You can put the hardware signer either a) together with the paper seed backup, or b) in another secure location (if available)
""", # noqa: E501
no_translate=self.no_translate,
).format(number=self.TEXT_24_WORDS),
self.style_paragraph_left,
)
else:
instructions1 = Paragraph(
translate(
"pdf",
"""1. Attach the 'Recovery sheet' ({number} words) to this paper or duplicate the 12/24 words onto this paper.
2. Fold this paper at the line below
3. Put each paper in a different secure location, where only you have access
4. You can put the hardware signers either a) together with the corresponding paper seed backup, or b) each in yet another secure location (if available)
""", # noqa: E501
no_translate=self.no_translate,
).format(number=self.TEXT_24_WORDS),
self.style_paragraph_left,
)
# No photography icon
icon = svg_tools.get_QIcon("no-typing-icon.svg")
icon2 = svg_tools.get_QIcon("no-photography-icon.svg")
reportlab_icon = pilimage_to_reportlab(qicon_to_pil(icon), width=50, height=50)
reportlab_icon2 = pilimage_to_reportlab(qicon_to_pil(icon2), width=50, height=50)
self.elements.append(
self.create_table(
[[reportlab_icon], [instructions1], [reportlab_icon2]],
[60, 400, 60],
)
)
self.elements.append(Spacer(1, 5))
# Table title
table_title = translate(
"pdf",
"Secret seed words for a hardware signer: Never type into a computer. Never make a picture.",
no_translate=self.no_translate,
)
seed_placeholder = "___________________"
# split seed words if available
if seed:
seed_items = seed.split(" ")
seed_items = seed_items + [seed_placeholder for i in range(24 - len(seed_items))]
else:
seed_items = [seed_placeholder for i in range(24)]
# 24 words placeholder in three columns
data = [[table_title, "", ""]] # First row is the title
for i in range(8):
data.append([f"{i + j + 1} {seed_items[i + j]}" for j in [0, 8, 16]])
table = Table(data)
# Table styling with border and title formatting
table_style = TableStyle(
[
("BOX", (0, 0), (-1, -1), 1, colors.black),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("SPAN", (0, 0), (2, 0)), # Merging the cells of the title row
(
"BACKGROUND",
(0, 0),
(2, 0),
colors.grey,
), # Background color for the title
(
"TEXTCOLOR",
(0, 0),
(2, 0),
colors.whitesmoke,
), # Text color for the title
("FONTNAME", (0, 0), (2, 0), self.font_name), # Font for the title
("ALIGN", (0, 0), (-1, -1), "LEFT"), # Vertical alignment for all cells
("LINEBELOW", (0, -1), (-1, -1), 1, colors.black), # Add the closing horizontal line below
],
fontName=self.font_name,
)
table.setStyle(table_style)
table.hAlign = "CENTER"
self.elements.append(table)
self.elements.append(Spacer(1, 1))
description_text = Paragraph(
translate(
"pdf",
"{keystore_label} ({keystore_fingerprint}): {keystore_description}
"
"Instructions for the heirs:",
no_translate=self.no_translate,
).format(
keystore_description=keystore_description.replace("\n", "
"),
keystore_fingerprint=keystore_fingerprint,
keystore_label=keystore_label,
),
self.style_paragraph_left,
)
self.elements.append(
self.create_table(
[[reportlab_icon2], [description_text], [reportlab_icon]],
[60, 400, 60],
)
)
def _descriptor_part(
self,
wallet_descriptor_string: str,
threshold: int,
) -> None:
"""Descriptor part."""
qr_image = pilimage_to_reportlab(
QRGenerator.create_qr_PILimage(wallet_descriptor_string), width=200, height=200
)
if threshold > 1:
desc_str = Paragraph(
translate(
"pdf",
"The wallet descriptor (QR Code)
{wallet_descriptor_string}
"
"allows you to create a watch-only wallet to see your balance. "
"To spent from it you need {threshold} Seeds and the wallet descriptor.",
no_translate=self.no_translate,
).format(threshold=threshold, wallet_descriptor_string=wallet_descriptor_string),
self.style_paragraph,
)
else:
desc_str = Paragraph(
translate(
"pdf",
"The wallet descriptor (QR Code)
{wallet_descriptor_string}
allows "
"you to create a watch-only wallet to see your balance. "
"To spent from it you need the secret {number} words (Seed).",
no_translate=self.no_translate,
).format(number=self.TEXT_24_WORDS, wallet_descriptor_string=wallet_descriptor_string),
self.style_paragraph,
)
self.elements.append(self.create_table([[qr_image], [desc_str]], [250, 300]))
def _signer_footer(
self,
keystore_label: str,
keystore_fingerprint: str,
keystore_key_origin: str,
keystore_xpub: str,
hardware_signer: HardwareSigner | None = None,
) -> Paragraph | Table:
"""Create the signer metadata footer, optionally with a small device icon."""
keystore_info_text = Paragraph(
translate(
"pdf",
"{keystore_label}: Fingerprint: {keystore_fingerprint}, "
"Key origin: {keystore_key_origin}, {keystore_xpub}",
no_translate=self.no_translate,
).format(
keystore_label=keystore_label,
keystore_fingerprint=keystore_fingerprint,
keystore_key_origin=keystore_key_origin,
keystore_xpub=keystore_xpub,
),
self.style_paragraph_left,
)
if not hardware_signer:
return keystore_info_text
icon = svg_tools_hardware_signer.get_QIcon(hardware_signer.icon_name)
reportlab_icon = pilimage_to_reportlab(qicon_to_pil(icon), width=16, height=16)
footer = Table(
[[reportlab_icon, keystore_info_text]],
colWidths=[22, 500],
)
footer.setStyle(
TableStyle(
[
("BOX", (0, 0), (-1, -1), 0, colors.white),
("INNERGRID", (0, 0), (-1, -1), 0, colors.white),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("ALIGN", (0, 0), (0, 0), "CENTER"),
("ALIGN", (1, 0), (1, 0), "LEFT"),
("LEFTPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 0),
("TOPPADDING", (0, 0), (-1, -1), 0),
("BOTTOMPADDING", (0, 0), (-1, -1), 0),
]
)
)
return footer
def create_pdf(
self,
title: str,
wallet_descriptor_string: str,
keystore_description: str,
keystore_label: str,
keystore_xpub: str,
keystore_key_origin: str,
keystore_fingerprint: str,
threshold: int,
seed: str | None = None,
num_signers: int = 1,
hardware_signer: HardwareSigner | None = None,
) -> None:
"""Create pdf."""
self.elements.append(Paragraph(title, style=self.style_heading))
# Small subtitle
self.elements.append(
Paragraph(
translate("pdf", "Created with", no_translate=self.no_translate)
+ f" {APP_NAME}: {white_space * 5} www.bitcoin-safe.org",
self.style_paragraph,
)
)
self.elements.append(Paragraph("", self.style_paragraph))
self._seed_part(
seed,
num_signers=num_signers,
keystore_label=keystore_label,
keystore_fingerprint=keystore_fingerprint,
keystore_description=keystore_description,
)
self.elements.append(Spacer(1, 15))
# Add a horizontal line as an element
line = Paragraph(
"________________________________________________________________________________",
self.style_paragraph,
)
# line.keepWithNext = True # Ensure line and text stay together on the same page
self.elements.append(line)
# Add text at the line as an element
text = (white_space * 5).join(
[translate("pdf", "Please fold here!", no_translate=self.no_translate)] * 5
)
text_paragraph = Paragraph(text, style=self.style_text)
# text_paragraph.spaceBefore = -10 # Adjust the space before the text if needed
self.elements.append(text_paragraph)
self._descriptor_part(wallet_descriptor_string, threshold)
self.elements.append(
self._signer_footer(
keystore_label=keystore_label,
keystore_fingerprint=keystore_fingerprint,
keystore_key_origin=keystore_key_origin,
keystore_xpub=keystore_xpub,
hardware_signer=hardware_signer,
)
)
def make_and_open_pdf(
wallet: Wallet, lang_code: str, selected_keystore_indexes: list[int] | None = None
) -> None:
"""Make and open pdf."""
info = DescriptorInfo.from_str(str(wallet.multipath_descriptor))
pdf_recovery = BitcoinWalletRecoveryPDF(lang_code=lang_code)
selected_indexes = (
[index for index in selected_keystore_indexes if 0 <= index < len(wallet.keystores)]
if selected_keystore_indexes is not None
else list(range(len(wallet.keystores)))
)
if not selected_indexes:
return
for position, i in enumerate(selected_indexes):
keystore = wallet.keystores[i]
title = (
translate(
"pdf",
'{i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}"',
no_translate=pdf_recovery.no_translate,
).format(i=i + 1, threshold=info.threshold, m=len(wallet.keystores), id=wallet.id)
if len(wallet.keystores) > 1
else f"{wallet.id}"
)
hardware_signer = HardwareSigners.from_id(keystore.hardware_signer_id)
if hardware_signer == HardwareSigners.generic:
hardware_signer = None
elif (
hardware_signer
and not Path(svg_tools_hardware_signer.get_icon_path(hardware_signer.icon_name)).exists()
):
hardware_signer = None
pdf_recovery.create_pdf(
title=title,
wallet_descriptor_string=str(wallet.multipath_descriptor),
threshold=info.threshold,
seed=keystore.mnemonic,
num_signers=len(wallet.keystores),
keystore_xpub=keystore.xpub,
keystore_description=keystore.description,
keystore_fingerprint=keystore.fingerprint,
keystore_key_origin=keystore.key_origin,
keystore_label=wallet.hardware_signer_label(i),
hardware_signer=hardware_signer,
)
if position < len(selected_indexes) - 1:
pdf_recovery.add_page_break()
filename = translate("pdf", "Seed backup of {id}").format(id=wallet.id)
write_and_open_temp_pdf(pdf_recovery, filename)