Add PDF download for interlinear chapter pages

- Create chapter_interlinear_pdf.html template for PDF export
- Add /book/{book}/chapter/{chapter}/interlinear/pdf route
- Add PDF download button to interlinear page alongside print
- Include Hebrew/Greek text, English translations, and Strong's numbers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-29 23:33:17 -05:00
parent 3177b097a4
commit d829041796
3 changed files with 229 additions and 1 deletions
+54 -1
View File
@@ -2239,6 +2239,57 @@ async def chapter_pdf(request: Request, book: str, chapter: int):
)
@app.get("/book/{book}/chapter/{chapter}/interlinear/pdf")
async def chapter_interlinear_pdf(book: str, chapter: int):
"""Generate PDF export for interlinear chapter view."""
# Redirect book name variations to canonical form
canonical_name = normalize_book_name(book)
if canonical_name:
return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/interlinear/pdf", status_code=301)
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
verses = bible.get_verses_by_book_chapter(book, chapter)
chapters = bible.get_chapters_for_book(book)
if not verses:
if not chapters:
raise HTTPException(status_code=404, detail=f"The book '{book}' was not found.")
else:
raise HTTPException(status_code=404, detail=f"Chapter {chapter} of {book} was not found.")
# Get interlinear data for each verse
verses_with_interlinear = []
for verse in verses:
interlinear_words = get_interlinear_data(book, chapter, verse.verse)
verses_with_interlinear.append({
'verse': verse,
'interlinear_words': interlinear_words or []
})
# Determine if OT or NT for language badge
is_old_testament = book in OT_BOOKS
html_content = templates.get_template("chapter_interlinear_pdf.html").render(
book=book,
chapter=chapter,
verses_with_interlinear=verses_with_interlinear,
is_old_testament=is_old_testament
)
pdf_buffer = await render_html_to_pdf_async(html_content)
filename = f"{book.lower().replace(' ', '-')}-{chapter}-interlinear.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@app.get("/book/{book}/chapter/{chapter}/interlinear", response_class=HTMLResponse)
def read_chapter_interlinear(request: Request, book: str, chapter: int):
"""Display a chapter with interlinear Hebrew/Greek for every verse"""
@@ -2302,7 +2353,9 @@ def read_chapter_interlinear(request: Request, book: str, chapter: int):
"breadcrumbs": breadcrumbs,
"current_book": book,
"current_chapter": chapter,
"is_old_testament": is_old_testament
"is_old_testament": is_old_testament,
"pdf_available": WEASYPRINT_AVAILABLE,
"pdf_url": f"/book/{book}/chapter/{chapter}/interlinear/pdf" if WEASYPRINT_AVAILABLE else None
}
)
@@ -638,6 +638,14 @@
</div>
<div class="print-actions">
{% if pdf_available and pdf_url %}
<a href="{{ pdf_url }}" class="print-btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Download PDF
</a>
{% endif %}
<button onclick="window.print()" class="print-btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ book }} {{ chapter }} - Interlinear (KJV)</title>
<style>
@page {
size: letter;
margin: 0.75in;
@bottom-center {
content: counter(page);
font-family: "et-book", Georgia, serif;
font-size: 10pt;
color: #666;
}
}
body {
font-family: "et-book", Georgia, "Times New Roman", serif;
font-size: 10pt;
line-height: 1.5;
color: #111;
}
h1 {
font-weight: normal;
letter-spacing: 0.05em;
font-size: 22pt;
margin: 0 0 0.05in 0;
}
.subtitle {
font-style: italic;
color: #666;
margin-bottom: 0.25in;
font-size: 11pt;
}
.language-note {
font-size: 9pt;
color: #666;
margin-bottom: 0.3in;
border-bottom: 1px solid #ddd;
padding-bottom: 0.1in;
}
.verse-section {
margin-bottom: 0.3in;
page-break-inside: avoid;
}
.verse-header {
margin-bottom: 0.1in;
}
.verse-number {
font-size: 11pt;
font-weight: 700;
color: #4a7c59;
margin-right: 0.1in;
}
.verse-text {
font-size: 11pt;
font-style: italic;
color: #333;
}
.interlinear-flow {
margin: 0.1in 0;
}
.word-unit {
display: inline-block;
text-align: center;
margin: 0 0.03in 0.12in 0.03in;
vertical-align: top;
padding: 0.03in 0.05in;
}
.word-original {
display: block;
font-size: 14pt;
font-weight: 400;
color: #222;
margin-bottom: 0.02in;
line-height: 1.3;
}
.word-original.hebrew {
direction: rtl;
font-family: "SBL Hebrew", "Ezra SIL", "Times New Roman", serif;
font-size: 15pt;
}
.word-original.greek {
font-family: "SBL Greek", "Gentium Plus", "Times New Roman", serif;
font-size: 14pt;
}
.word-english {
display: block;
font-size: 8pt;
color: #4a7c59;
font-weight: 600;
line-height: 1.2;
}
.word-strongs {
display: block;
font-size: 6pt;
color: #888;
font-family: monospace;
margin-top: 0.01in;
}
.no-interlinear {
color: #888;
font-style: italic;
font-size: 9pt;
}
.footer {
margin-top: 0.4in;
font-size: 9pt;
color: #888;
text-align: center;
}
</style>
</head>
<body>
<h1>{{ book }} {{ chapter }}</h1>
<p class="subtitle">Interlinear Bible &mdash; King James Version</p>
<p class="language-note">
{% if is_old_testament %}Hebrew{% else %}Greek{% endif %} text with English translation and Strong's numbers
</p>
{% for item in verses_with_interlinear %}
<div class="verse-section">
<div class="verse-header">
<span class="verse-number">{{ item.verse.verse }}</span>
<span class="verse-text">{{ item.verse.text }}</span>
</div>
{% if item.interlinear_words %}
<div class="interlinear-flow">
{% for word in item.interlinear_words %}
<span class="word-unit">
<span class="word-original {% if is_old_testament %}hebrew{% else %}greek{% endif %}">{{ word.original }}</span>
<span class="word-english">{{ word.english }}</span>
{% if word.strongs %}
<span class="word-strongs">{{ word.strongs }}</span>
{% endif %}
</span>
{% endfor %}
</div>
{% else %}
<p class="no-interlinear">Interlinear data not available for this verse.</p>
{% endif %}
</div>
{% endfor %}
<div class="footer">
From KJV Study &bull; kjvstudy.org
</div>
</body>
</html>