Compare commits
1 Commits
main
...
62aa32c13d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62aa32c13d |
@@ -15,9 +15,16 @@ class MeasurementEngine(
|
|||||||
// Title height
|
// Title height
|
||||||
heightMm += fontMetrics.measureLineHeight(config.fonts.title, config.fonts.title.size) * 1.5f
|
heightMm += fontMetrics.measureLineHeight(config.fonts.title, config.fonts.title.size) * 1.5f
|
||||||
|
|
||||||
// Metadata line (composer/lyricist)
|
// Metadata lines (composer/lyricist) - may be 1 or 2 lines depending on label style
|
||||||
if (song.composer != null || song.lyricist != null) {
|
if (song.composer != null || song.lyricist != null) {
|
||||||
heightMm += fontMetrics.measureLineHeight(config.fonts.metadata, config.fonts.metadata.size) * 1.8f
|
val metaLineHeight = fontMetrics.measureLineHeight(config.fonts.metadata, config.fonts.metadata.size) * 1.8f
|
||||||
|
val useGerman = config.layout.metadataLabels == "german"
|
||||||
|
if (useGerman && song.lyricist != null && song.composer != null && song.lyricist != song.composer) {
|
||||||
|
// Two separate lines: "Worte: ..." and "Weise: ..."
|
||||||
|
heightMm += metaLineHeight * 2
|
||||||
|
} else {
|
||||||
|
heightMm += metaLineHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key/capo line
|
// Key/capo line
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ data class LayoutConfig(
|
|||||||
val margins: Margins = Margins(),
|
val margins: Margins = Margins(),
|
||||||
val chordLineSpacing: Float = 3f, // mm
|
val chordLineSpacing: Float = 3f, // mm
|
||||||
val verseSpacing: Float = 4f, // mm
|
val verseSpacing: Float = 4f, // mm
|
||||||
val pageNumberPosition: String = "bottom-outer"
|
val pageNumberPosition: String = "bottom-outer",
|
||||||
|
val metadataLabels: String = "abbreviated", // "abbreviated" (M:/T:) or "german" (Worte:/Weise:)
|
||||||
|
val metadataPosition: String = "top" // "top" (after title) or "bottom" (bottom of last page)
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Margins(
|
data class Margins(
|
||||||
|
|||||||
@@ -214,6 +214,31 @@ class ConfigParserTest {
|
|||||||
config.toc.highlightColumn.shouldBeNull()
|
config.toc.highlightColumn.shouldBeNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse config with german metadata labels`() {
|
||||||
|
val yaml = """
|
||||||
|
book:
|
||||||
|
title: "Test"
|
||||||
|
layout:
|
||||||
|
metadata_labels: german
|
||||||
|
metadata_position: bottom
|
||||||
|
""".trimIndent()
|
||||||
|
val config = ConfigParser.parse(yaml)
|
||||||
|
config.layout.metadataLabels shouldBe "german"
|
||||||
|
config.layout.metadataPosition shouldBe "bottom"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse config with default metadata settings`() {
|
||||||
|
val yaml = """
|
||||||
|
book:
|
||||||
|
title: "Test"
|
||||||
|
""".trimIndent()
|
||||||
|
val config = ConfigParser.parse(yaml)
|
||||||
|
config.layout.metadataLabels shouldBe "abbreviated"
|
||||||
|
config.layout.metadataPosition shouldBe "top"
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `parse config ignores unknown properties`() {
|
fun `parse config ignores unknown properties`() {
|
||||||
val yaml = """
|
val yaml = """
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ class PdfBookRenderer : BookRenderer {
|
|||||||
) {
|
) {
|
||||||
var y = contentTop
|
var y = contentTop
|
||||||
|
|
||||||
|
val renderMetaAtBottom = config.layout.metadataPosition == "bottom"
|
||||||
|
|
||||||
if (pageIndex == 0) {
|
if (pageIndex == 0) {
|
||||||
// Render title
|
// Render title
|
||||||
val titleFont = fontMetrics.getBaseFont(config.fonts.title)
|
val titleFont = fontMetrics.getBaseFont(config.fonts.title)
|
||||||
@@ -116,20 +118,22 @@ class PdfBookRenderer : BookRenderer {
|
|||||||
cb.endText()
|
cb.endText()
|
||||||
y -= titleSize * 1.5f
|
y -= titleSize * 1.5f
|
||||||
|
|
||||||
// Render metadata line (composer/lyricist)
|
// Render metadata line (composer/lyricist) - at top position only
|
||||||
val metaParts = mutableListOf<String>()
|
if (!renderMetaAtBottom) {
|
||||||
song.composer?.let { metaParts.add("M: $it") }
|
val metaParts = buildMetadataLines(song, config)
|
||||||
song.lyricist?.let { metaParts.add("T: $it") }
|
if (metaParts.isNotEmpty()) {
|
||||||
if (metaParts.isNotEmpty()) {
|
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
|
for (metaLine in metaParts) {
|
||||||
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, y - metaSize)
|
||||||
cb.showText(metaParts.joinToString(" / "))
|
cb.showText(metaLine)
|
||||||
cb.endText()
|
cb.endText()
|
||||||
y -= metaSize * 1.8f
|
y -= metaSize * 1.8f
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render key and capo
|
// Render key and capo
|
||||||
@@ -242,6 +246,28 @@ class PdfBookRenderer : BookRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render metadata at bottom of song page (if configured)
|
||||||
|
if (renderMetaAtBottom && pageIndex == 0) {
|
||||||
|
val metaParts = buildMetadataLines(song, config)
|
||||||
|
if (metaParts.isNotEmpty()) {
|
||||||
|
y -= 4f
|
||||||
|
val metaFont = fontMetrics.getBaseFont(config.fonts.metadata)
|
||||||
|
val metaSize = config.fonts.metadata.size
|
||||||
|
for (metaLine in metaParts) {
|
||||||
|
val wrappedLines = wrapText(metaLine, metaFont, metaSize, contentWidth)
|
||||||
|
for (wrappedLine in wrappedLines) {
|
||||||
|
cb.beginText()
|
||||||
|
cb.setFontAndSize(metaFont, metaSize)
|
||||||
|
cb.setColorFill(Color.GRAY)
|
||||||
|
cb.setTextMatrix(leftMargin, y - metaSize)
|
||||||
|
cb.showText(wrappedLine)
|
||||||
|
cb.endText()
|
||||||
|
y -= metaSize * 1.5f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render reference book footer on the last page of the song
|
// Render reference book footer on the last page of the song
|
||||||
if (config.referenceBooks.isNotEmpty() && song.references.isNotEmpty()) {
|
if (config.referenceBooks.isNotEmpty() && song.references.isNotEmpty()) {
|
||||||
val isLastPage = (pageIndex == 0) // For now, all content renders on page 0
|
val isLastPage = (pageIndex == 0) // For now, all content renders on page 0
|
||||||
@@ -255,6 +281,35 @@ class PdfBookRenderer : BookRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build metadata lines based on configured label style.
|
||||||
|
* Returns a list of lines to render (may be empty).
|
||||||
|
*/
|
||||||
|
private fun buildMetadataLines(song: Song, config: BookConfig): List<String> {
|
||||||
|
val useGerman = config.layout.metadataLabels == "german"
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (useGerman) {
|
||||||
|
// German labels: "Worte und Weise:" when same person, otherwise separate
|
||||||
|
if (song.lyricist != null && song.composer != null && song.lyricist == song.composer) {
|
||||||
|
lines.add("Worte und Weise: ${song.lyricist}")
|
||||||
|
} else {
|
||||||
|
song.lyricist?.let { lines.add("Worte: $it") }
|
||||||
|
song.composer?.let { lines.add("Weise: $it") }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Abbreviated labels on a single line
|
||||||
|
val parts = mutableListOf<String>()
|
||||||
|
song.composer?.let { parts.add("M: $it") }
|
||||||
|
song.lyricist?.let { parts.add("T: $it") }
|
||||||
|
if (parts.isNotEmpty()) {
|
||||||
|
lines.add(parts.joinToString(" / "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders reference book abbreviations and page numbers as a footer row
|
* Renders reference book abbreviations and page numbers as a footer row
|
||||||
* at the bottom of the song page, above the page number.
|
* at the bottom of the song page, above the page number.
|
||||||
|
|||||||
Reference in New Issue
Block a user