feat: anchor footer elements to bottom of page (Closes #31) #32
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user