Initial implementation of songbook toolset
Kotlin/JVM multi-module project for generating a scout songbook PDF from ChordPro-format text files. Includes ChordPro parser, layout engine with greedy spread packing for double-page songs, OpenPDF renderer, CLI (Clikt), Compose Desktop GUI, and 5 sample songs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
3
model/build.gradle.kts
Normal file
3
model/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
id("songbook-conventions")
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package de.pfadfinder.songbook.model
|
||||
|
||||
data class BookConfig(
|
||||
val book: BookMeta = BookMeta(),
|
||||
val songs: SongsConfig = SongsConfig(),
|
||||
val fonts: FontsConfig = FontsConfig(),
|
||||
val layout: LayoutConfig = LayoutConfig(),
|
||||
val images: ImagesConfig = ImagesConfig(),
|
||||
val referenceBooks: List<ReferenceBook> = emptyList(),
|
||||
val output: OutputConfig = OutputConfig()
|
||||
)
|
||||
|
||||
data class BookMeta(
|
||||
val title: String = "Liederbuch",
|
||||
val subtitle: String? = null,
|
||||
val edition: String? = null,
|
||||
val format: String = "A5"
|
||||
)
|
||||
|
||||
data class SongsConfig(
|
||||
val directory: String = "./songs",
|
||||
val order: String = "alphabetical" // "alphabetical" or "manual"
|
||||
)
|
||||
|
||||
data class FontsConfig(
|
||||
val lyrics: FontSpec = FontSpec(family = "Helvetica", size = 10f),
|
||||
val chords: FontSpec = FontSpec(family = "Helvetica", size = 9f, color = "#333333"),
|
||||
val title: FontSpec = FontSpec(family = "Helvetica", size = 14f),
|
||||
val metadata: FontSpec = FontSpec(family = "Helvetica", size = 8f),
|
||||
val toc: FontSpec = FontSpec(family = "Helvetica", size = 9f)
|
||||
)
|
||||
|
||||
data class FontSpec(
|
||||
val family: String = "Helvetica",
|
||||
val file: String? = null,
|
||||
val size: Float = 10f,
|
||||
val color: String = "#000000"
|
||||
)
|
||||
|
||||
data class LayoutConfig(
|
||||
val margins: Margins = Margins(),
|
||||
val chordLineSpacing: Float = 3f, // mm
|
||||
val verseSpacing: Float = 4f, // mm
|
||||
val pageNumberPosition: String = "bottom-outer"
|
||||
)
|
||||
|
||||
data class Margins(
|
||||
val top: Float = 15f,
|
||||
val bottom: Float = 15f,
|
||||
val inner: Float = 20f,
|
||||
val outer: Float = 12f
|
||||
)
|
||||
|
||||
data class ImagesConfig(
|
||||
val directory: String = "./images"
|
||||
)
|
||||
|
||||
data class ReferenceBook(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val abbreviation: String
|
||||
)
|
||||
|
||||
data class OutputConfig(
|
||||
val directory: String = "./output",
|
||||
val filename: String = "liederbuch.pdf"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.pfadfinder.songbook.model
|
||||
|
||||
import java.io.OutputStream
|
||||
|
||||
interface BookRenderer {
|
||||
fun render(layout: LayoutResult, config: BookConfig, output: OutputStream)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.pfadfinder.songbook.model
|
||||
|
||||
interface FontMetrics {
|
||||
fun measureTextWidth(text: String, font: FontSpec, size: Float): Float
|
||||
fun measureLineHeight(font: FontSpec, size: Float): Float
|
||||
}
|
||||
26
model/src/main/kotlin/de/pfadfinder/songbook/model/Layout.kt
Normal file
26
model/src/main/kotlin/de/pfadfinder/songbook/model/Layout.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package de.pfadfinder.songbook.model
|
||||
|
||||
data class MeasuredSong(
|
||||
val song: Song,
|
||||
val totalHeightMm: Float,
|
||||
val pageCount: Int // 1 or 2
|
||||
)
|
||||
|
||||
sealed class PageContent {
|
||||
data class SongPage(val song: Song, val pageIndex: Int) : PageContent() // pageIndex 0 or 1 for 2-page songs
|
||||
data class FillerImage(val imagePath: String) : PageContent()
|
||||
data object BlankPage : PageContent()
|
||||
}
|
||||
|
||||
data class LayoutResult(
|
||||
val tocPages: Int,
|
||||
val pages: List<PageContent>,
|
||||
val tocEntries: List<TocEntry>
|
||||
)
|
||||
|
||||
data class TocEntry(
|
||||
val title: String,
|
||||
val pageNumber: Int,
|
||||
val isAlias: Boolean = false,
|
||||
val references: Map<String, Int> = emptyMap() // bookAbbrev → page
|
||||
)
|
||||
31
model/src/main/kotlin/de/pfadfinder/songbook/model/Song.kt
Normal file
31
model/src/main/kotlin/de/pfadfinder/songbook/model/Song.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
package de.pfadfinder.songbook.model
|
||||
|
||||
data class Song(
|
||||
val title: String,
|
||||
val aliases: List<String> = emptyList(),
|
||||
val lyricist: String? = null,
|
||||
val composer: String? = null,
|
||||
val key: String? = null,
|
||||
val tags: List<String> = emptyList(),
|
||||
val notes: List<String> = emptyList(),
|
||||
val references: Map<String, Int> = emptyMap(), // bookId → page number
|
||||
val capo: Int? = null,
|
||||
val sections: List<SongSection> = emptyList()
|
||||
)
|
||||
|
||||
data class SongSection(
|
||||
val type: SectionType,
|
||||
val label: String? = null,
|
||||
val lines: List<SongLine> = emptyList()
|
||||
)
|
||||
|
||||
enum class SectionType {
|
||||
VERSE, CHORUS, BRIDGE, REPEAT
|
||||
}
|
||||
|
||||
data class SongLine(val segments: List<LineSegment>)
|
||||
|
||||
data class LineSegment(
|
||||
val chord: String? = null, // null = no chord above this segment
|
||||
val text: String
|
||||
)
|
||||
Reference in New Issue
Block a user