#!/usr/bin/env python3 """Generate an EPUB from USER_GUIDE.md.""" import html as _html import io import re from pathlib import Path import markdown from ebooklib import epub from PIL import Image, ImageDraw, ImageFont ROOT = Path(__file__).parent.parent SOURCE_MD = ROOT / "USER_GUIDE.md" OUTPUT_EPUB = ROOT / "CrossPoint_User_Guide.epub" LOGO_PNG = ROOT / "src/images/Logo120.png" # Portrait cover dimensions matching the X4 display (480×800) COVER_W, COVER_H = 480, 800 CSS = """ body { font-family: Georgia, 'Times New Roman', serif; font-size: 1em; line-height: 1.6; margin: 1em 1.5em; color: #111; } h1 { font-size: 1.8em; margin-top: 1.2em; margin-bottom: 0.4em; } h2 { font-size: 1.4em; margin-top: 1.5em; margin-bottom: 0.3em; border-bottom: 1px solid #ccc; padding-bottom: 0.2em; } h3 { font-size: 1.2em; margin-top: 1.2em; margin-bottom: 0.2em; } h4 { font-size: 1.05em; margin-top: 1em; margin-bottom: 0.2em; } h5 { font-size: 1em; margin-top: 0.8em; margin-bottom: 0.2em; } p { margin: 0.6em 0; } code { font-family: 'Courier New', Courier, monospace; font-size: 0.85em; background: #f4f4f4; padding: 0.1em 0.3em; border-radius: 3px; } pre { font-family: 'Courier New', Courier, monospace; font-size: 0.8em; background: #f4f4f4; border: 1px solid #ddd; border-radius: 4px; padding: 0.8em 1em; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; } pre code { background: none; padding: 0; border-radius: 0; font-size: 1em; } table { border-collapse: collapse; width: 100%; margin: 0.8em 0; font-size: 0.9em; } th, td { border: 1px solid #ccc; padding: 0.4em 0.6em; text-align: left; } th { background: #eee; font-weight: bold; } ul, ol { margin: 0.5em 0; padding-left: 1.8em; } li { margin: 0.25em 0; } a { color: #1a6699; text-decoration: none; } hr { border: none; border-top: 1px solid #ccc; margin: 1.5em 0; } .callout { border-left: 4px solid #888; background: #f8f8f8; padding: 0.5em 0.8em; margin: 0.8em 0; border-radius: 0 4px 4px 0; } .callout-note { border-color: #2196F3; background: #e3f2fd; } .callout-tip { border-color: #4CAF50; background: #e8f5e9; } .callout-warning { border-color: #FF9800; background: #fff3e0; } .callout-title { font-weight: bold; margin-bottom: 0.3em; } /* Cover page */ .cover-page { text-align: center; margin: 0; padding: 0; } .cover-page img { width: 100%; height: auto; display: block; margin: 0; } """ def make_cover_png() -> bytes: """Generate a full-size (480×800) cover image with logo, title, and subtitle.""" cover = Image.new('RGB', (COVER_W, COVER_H), color=(255, 255, 255)) draw = ImageDraw.Draw(cover) # Paste logo centred at ~35% down the canvas with Image.open(LOGO_PNG) as logo_raw: if logo_raw.mode in ('RGBA', 'LA', 'P'): bg = Image.new('RGB', logo_raw.size, (255, 255, 255)) bg.paste(logo_raw, mask=logo_raw.convert('RGBA').split()[3]) logo = bg else: logo = logo_raw.convert('RGB') # Scale logo to 240×240 (half the cover width) logo = logo.resize((240, 240), Image.LANCZOS) logo_x = (COVER_W - 240) // 2 logo_y = int(COVER_H * 0.28) cover.paste(logo, (logo_x, logo_y)) # Try to use a system font; fall back to default if unavailable try: font_title = ImageFont.truetype('/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf', 52) font_sub = ImageFont.truetype('/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf', 32) except OSError: font_title = ImageFont.load_default() font_sub = font_title title = 'CrossPoint' subtitle = 'User Guide' # Draw title text bbox = draw.textbbox((0, 0), title, font=font_title) tw = bbox[2] - bbox[0] ty = logo_y + 240 + 48 draw.text(((COVER_W - tw) // 2, ty), title, fill=(0, 0, 0), font=font_title) # Draw subtitle text bbox2 = draw.textbbox((0, 0), subtitle, font=font_sub) sw = bbox2[2] - bbox2[0] sy = ty + (bbox[3] - bbox[1]) + 18 draw.text(((COVER_W - sw) // 2, sy), subtitle, fill=(80, 80, 80), font=font_sub) # Thin horizontal rules above and below the text block rule_y1 = ty - 24 rule_y2 = sy + (bbox2[3] - bbox2[1]) + 24 draw.line([(60, rule_y1), (COVER_W - 60, rule_y1)], fill=(180, 180, 180), width=1) draw.line([(60, rule_y2), (COVER_W - 60, rule_y2)], fill=(180, 180, 180), width=1) buf = io.BytesIO() cover.save(buf, format='PNG') return buf.getvalue() def preprocess_callouts(text: str) -> str: """Convert GitHub-style > [!TYPE] callouts to HTML divs.""" lines = text.splitlines() out = [] i = 0 while i < len(lines): line = lines[i] m = re.match(r'^>\s*\[!(NOTE|TIP|WARNING)\]\s*$', line.strip()) if m: kind = m.group(1).lower() title = kind.capitalize() body_lines = [] i += 1 while i < len(lines) and lines[i].startswith('>') \ and not re.match(r'^>\s*\[!', lines[i]): body_lines.append(lines[i][1:].lstrip()) i += 1 body = _html.escape('\n'.join(body_lines).strip()) out.append( f'\n
{body}
' f'
'
'