feat: anchor footer elements to bottom of page (Closes #31) #32

Merged
shahondin1624 merged 1 commits from feature/issue-31-anchor-footer-elements-to-bottom-of-page into main 2026-03-17 16:21:00 +01:00

View File

@@ -424,62 +424,70 @@ class PdfBookRenderer : BookRenderer {
y -= config.layout.verseSpacing / 0.3528f y -= config.layout.verseSpacing / 0.3528f
} }
// Render notes on the last page // Render footer elements (notes, metadata, references) anchored to the bottom of the page.
if (isLastPage && song.notes.isNotEmpty()) { // Instead of flowing from the current y position after song content, we compute a fixed
y -= 4f // starting Y at the top of the footer area (bottomMargin + footerReservation) and render
// top-down: notes -> metadata -> references. This ensures footer elements always appear
// at the same vertical position regardless of how much song content is on the page.
if (isLastPage && footerReservation > 0f) {
val metaFont = fontMetrics.getBaseFont(config.fonts.metadata) val metaFont = fontMetrics.getBaseFont(config.fonts.metadata)
val metaSize = config.fonts.metadata.size val metaSize = config.fonts.metadata.size
val noteLineHeight = metaSize * 1.5f
// The footer area spans from bottomMargin to bottomMargin + footerReservation.
// Start rendering from the top of this area, flowing downward.
var footerY = bottomMargin + footerReservation
// Render notes (topmost footer element)
if (song.notes.isNotEmpty()) {
footerY -= 4f // gap before notes
val noteLineHeight = metaSize * 1.5f
for ((idx, note) in song.notes.withIndex()) { for ((idx, note) in song.notes.withIndex()) {
val wrappedLines = wrapText(note, metaFont, metaSize, contentWidth) val wrappedLines = wrapText(note, metaFont, metaSize, contentWidth)
for (wrappedLine in wrappedLines) { for (wrappedLine in wrappedLines) {
cb.beginText() cb.beginText()
cb.setFontAndSize(metaFont, metaSize) cb.setFontAndSize(metaFont, metaSize)
cb.setColorFill(Color.GRAY) cb.setColorFill(Color.GRAY)
cb.setTextMatrix(leftMargin, y - metaSize) cb.setTextMatrix(leftMargin, footerY - metaSize)
cb.showText(wrappedLine) cb.showText(wrappedLine)
cb.endText() cb.endText()
y -= noteLineHeight footerY -= noteLineHeight
} }
// Add paragraph spacing between note paragraphs
if (idx < song.notes.size - 1) { if (idx < song.notes.size - 1) {
y -= noteLineHeight * 0.3f footerY -= noteLineHeight * 0.3f
} }
} }
} }
// Render metadata at bottom of song page (if configured) - on the last page only // Render metadata (Worte/Weise) below notes, if configured at bottom
if (renderMetaAtBottom && isLastPage) { if (renderMetaAtBottom) {
val metaParts = buildMetadataLines(song, config) val metaParts = buildMetadataLines(song, config)
if (metaParts.isNotEmpty()) { if (metaParts.isNotEmpty()) {
y -= 4f footerY -= 4f // gap before metadata
val metaFont = fontMetrics.getBaseFont(config.fonts.metadata)
val metaSize = config.fonts.metadata.size
for (metaLine in metaParts) { for (metaLine in metaParts) {
val wrappedLines = wrapText(metaLine, metaFont, metaSize, contentWidth) val wrappedLines = wrapText(metaLine, metaFont, metaSize, contentWidth)
for (wrappedLine in wrappedLines) { for (wrappedLine in wrappedLines) {
cb.beginText() cb.beginText()
cb.setFontAndSize(metaFont, metaSize) cb.setFontAndSize(metaFont, metaSize)
cb.setColorFill(Color.GRAY) cb.setColorFill(Color.GRAY)
cb.setTextMatrix(leftMargin, y - metaSize) cb.setTextMatrix(leftMargin, footerY - metaSize)
cb.showText(wrappedLine) cb.showText(wrappedLine)
cb.endText() cb.endText()
y -= metaSize * 1.5f footerY -= metaSize * 1.5f
} }
} }
} }
} }
// Render reference book footer on the last page of the song // Render reference book footer (bottommost footer element, just above page number)
if (config.referenceBooks.isNotEmpty() && song.references.isNotEmpty() && isLastPage) { if (config.referenceBooks.isNotEmpty() && song.references.isNotEmpty()) {
y -= 4f // gap before reference footer footerY -= 4f // gap before reference footer
renderReferenceFooter( renderReferenceFooter(
cb, fontMetrics, config, song, cb, fontMetrics, config, song,
leftMargin, y, contentWidth leftMargin, footerY, contentWidth
) )
} }
} }
}
/** /**
* Build metadata lines based on configured label style. * Build metadata lines based on configured label style.