Sync Column count with measure(multiple) on print

This commit is contained in:
Hasinjato 2026-09-17 15:26:26 +03:00
parent 6d5f4cc15c
commit 6a0c3e4e0c
2 changed files with 121 additions and 27 deletions

View file

@ -88,8 +88,8 @@ private fun bestColWidthPt(
}
}
//lyrics
(1..2).forEach { index ->
tuo.lyricsAsMultiString(index)
//(1..2).forEach { index ->
tuo.lyricsAsMultiString(1)
.split("\n")
.forEach { line ->
val trimmedLine = cleanSyllable(line)
@ -98,7 +98,7 @@ private fun bestColWidthPt(
maxLyrW = x
}
}
}
//}
}
val minRequiredByLyrics = maxLyrW * 0.65f
@ -453,7 +453,8 @@ actual fun rememberPdfExportAction(
scope.launch(Dispatchers.IO) {
val (pdfBytes, computedFileName) = generatePdfToBytes(
gridData, songTitle, measure, stanza,
nbStanza, songKey, songAut, songComp, songRythm
nbStanza, songKey, songAut, songComp, songRythm,
settings
)
val chosenPath = withContext(Dispatchers.Main) {
fileRepository.pickSavePath(computedFileName, "Exporter en PDF")
@ -520,8 +521,16 @@ private suspend fun generatePdfToBytes(
// ── Largeur optimale de colonne ────────────────────────────────────────
val bestColW = bestColWidthPt(tuoList, customFont, noteFontSize, lyricFont, lyricFontSize)
val numerator = measure.substringBefore("/").trim().toIntOrNull() ?: 4
val multiple = when (numerator) {
2, 4, 8, 10 -> 4
3, 6, 9, 12 -> 3
else -> 4
}
val colCount = if (customNbGrid == 0) {
((usableWidth / bestColW).toInt()).coerceAtLeast(1)
val rawColsInt = (usableWidth / bestColW).toInt()
val roundedToMultiple = (rawColsInt / multiple) * multiple
roundedToMultiple.coerceAtLeast(multiple)
} else {
customNbGrid
}
@ -757,7 +766,19 @@ private suspend fun generatePdfToBytes(
nbStanza = nbStanza
)
val rows = tuoList.chunked(colCount)
val pitchPattern = "\\b(d|di|r|ri|m|mi|f|fi|s|si|l|la|t|ta)\\b".toRegex()
fun isRowEmptyOrSeparatorOnly(rowTuos: List<TimeUnitObject>): Boolean {
return rowTuos.all { tuo ->
val noteContent = tuo.noteAsMultiString()
!pitchPattern.containsMatchIn(noteContent)
}
}
val allRows = tuoList.chunked(colCount)
val rows = if (allRows.size > 1 && isRowEmptyOrSeparatorOnly(allRows.last())) {
allRows.dropLastWhile { isRowEmptyOrSeparatorOnly(it) }
} else {
allRows
}
rows.forEachIndexed { rowIdx, rowTuos ->
val maxNoteLines = (rowTuos.maxOfOrNull { it.noteAsMultiString().split("\n").size } ?: 1) + 1
@ -776,7 +797,6 @@ private suspend fun generatePdfToBytes(
val notesStartY = if (rowHasMarkers) y - markerHeight else y
val pitchPattern = "\\b(d|di|r|ri|m|mi|f|fi|s|si|l|la|t|ta)\\b".toRegex()
fun singingVoiceIndices(tuo: TimeUnitObject): List<Int> =
tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line ->

View file

@ -84,35 +84,76 @@ private fun bestColWidthPt(
}
}
//lyrics
(1..2).forEach { index ->
tuo.lyricsAsMultiString(index)
/*(1..2).forEach { index ->*/
tuo.lyricsAsMultiString(1)
.split("\n")
.forEach { line ->
val trimmedLine = cleanSyllable(line)
val x = measureNoteWidth(font, lyricFontSize, trimmedLine)
val x = measureNoteWidth(font, lyricFontSize * 0.85f, trimmedLine)
if (x > maxLyrW) {
maxLyrW = x
// println("Lyr: [$trimmedLine] = [$maxLyrW]")
}
}
}
/*}*/
}
//La colonne fait la taille de la note + marge,
val minRequiredByLyrics = maxLyrW * 0.65f
val minRequiredByLyrics = maxLyrW * 0.85f
// println("La taille est = [${maxOf(maxNoteW + 2f, minRequiredByLyrics)}]")
return maxOf(maxNoteW + 2f, minRequiredByLyrics)
val finalWidth = maxOf(maxNoteW/* + 2f*/, minRequiredByLyrics)
// println("┌──────────────────────────────────────────────────────────┐")
// println("│ DEBUG: MEASURE COLUMNS & MAX WIDTH │")
// println("├──────────────────────────────────────────────────────────┤")
// println("│ Note max : \"${maxNoteW.toString()}\" -> Width: $maxNoteW pt (+2pt margin = ${maxNoteW/* + 2f*/} pt)")
// println("│ Lyric max : \"${maxLyrW.toString()}\" -> Width: $maxLyrW pt (Required * 0.85 = $minRequiredByLyrics pt)")
// println("│ Winner : ${if (maxNoteW + 2f >= minRequiredByLyrics) "NOTE" else "LYRIC"}")
// println("│ Best Col Width Retained : $finalWidth pt")
// println("└──────────────────────────────────────────────────────────┘")
return finalWidth
}
/** Mesure la largeur exacte comme drawNoteWithSubscript */
/** Mesure la largeur exacte en reproduisant le comportement de drawNoteWithModulation et drawNoteWithSubscript */
private fun measureNoteWidth(font: PDType0Font, fontSize: Float, text: String): Float {
if (text.isEmpty()) return 0f
// Gère le format avec modulation de type ">t>m" ou "[:d>{ta|d}]"
if (text.contains(">")) {
var totalW = 0f
text.forEach { ch ->
totalW += try {
font.getStringWidth(ch.toString().sanitize()) / 1000f * fontSize
} catch (e: Exception) { 0f }
val parts = text.split(">")
parts.forEachIndexed { index, part ->
println("i$index [$part]")
if (part.isNotEmpty()) {
val sanitizedPart = part.sanitize()
// Le premier élément après '>' est l'exposant/modulation (réduit à 0.7f)
val isExposant = index == 1
val currentSize = if (isExposant) fontSize * 0.5f else fontSize
totalW += textWidth(font, currentSize, sanitizedPart) + 0.5f
}
}
return totalW
}
var curX = 0f
for (char in text) {
val str = char.toString().sanitize()
val isSuperscript = char in setOf('¹', '²', '³', '⁴', '⁵')
val isSubscript = char in setOf('₁', '₂', '₃', '₄', '₅')
val isPunctuation = char in setOf('•', ',')
val currentSize = if (isSuperscript || isSubscript) fontSize * 0.6f else fontSize
val charWidth = try {
font.getStringWidth(str) / 1000f * currentSize
} catch (e: Exception) { 0f }
curX += when {
isSuperscript || isSubscript -> charWidth * 0.3f
isPunctuation -> charWidth
else -> charWidth + 0.5f
}
}
return curX
}
/** Equivalent de makeSpaceBetweenSyllables pour PDFBox */
private fun spacedSyllable(
currentSyl: String,
@ -729,8 +770,18 @@ private suspend fun generatePdfToBytes(
lyricFont,
lyricFontSize
)
val numerator = measure.substringBefore("/").trim().toIntOrNull() ?: 4
val multiple = when (numerator) {
2, 4, 8, 10 -> 4
3, 6, 9, 12 -> 3
else -> 4
}
val colCount = if (customNbGrid == 0) {
((usableWidth / bestColW).toInt()).coerceAtLeast(1)
val rawColsInt = (usableWidth / bestColW).toInt()
val roundedToMultiple = (rawColsInt / multiple) * multiple
roundedToMultiple.coerceAtLeast(multiple)
} else {
customNbGrid
}
@ -738,6 +789,16 @@ private suspend fun generatePdfToBytes(
//println("ici Colc est $colCount")
val colWidth = finalColWidth
// println("┌──────────────────────────────────────────────────────────┐")
// println("│ DEBUG: COLUMN COUNT CALCULATION │")
// println("├──────────────────────────────────────────────────────────┤")
// println("│ Usable Page Width : $usableWidth pt")
// println("│ Best Col Width : $bestColW pt")
// println("│ Raw Columns Count : ${(usableWidth / bestColW).toInt()}")
// println("│ Multiple Used : $multiple (Measure numerator: $numerator)")
// println("│ Final Col Count : $colCount")
// println("│ Final Col Width : $finalColWidth pt")
// println("└──────────────────────────────────────────────────────────┘")
val maxSylsSize = tuoList.maxOfOrNull { tuo -> // lyrics Alt
(1..nbStanza).maxOfOrNull { sz ->
@ -987,14 +1048,28 @@ private suspend fun generatePdfToBytes(
nbStanza = nbStanza
)
val pitchPattern = "\\b(d|di|r|ri|m|mi|f|fi|s|si|l|la|t|ta)\\b".toRegex()
fun isRowEmptyOrSeparatorOnly(rowTuos: List<TimeUnitObject>): Boolean {
return rowTuos.all { tuo ->
val noteContent = tuo.noteAsMultiString()
!pitchPattern.containsMatchIn(noteContent)
}
}
val allRows = tuoList.chunked(colCount)
val rows = if (allRows.size > 1 && isRowEmptyOrSeparatorOnly(allRows.last())) {
allRows.dropLastWhile { isRowEmptyOrSeparatorOnly(it) }
} else {
allRows
}
/*val allRows = tuoList.chunked(colCount)
// si dernier portée n'a qu'une séparateur pas de note on dessine sur l'avant dernier
val hasTrailingBarlineRow = allRows.size > 1 &&
*/val hasTrailingBarlineRow = allRows.size > 1 &&
allRows.last().size == 1 &&
allRows.last().first().noteAsMultiString().trim().isEmpty() &&
allRows.last().first().sep0 in listOf("|", "/")
allRows.last().first().sep0 in listOf("|", "/")/*
val rows = if (hasTrailingBarlineRow) allRows.dropLast(1) else allRows
val rows = if (hasTrailingBarlineRow) allRows.dropLast(1) else allRows*/
rows.forEachIndexed { rowIdx, rowTuos ->
// println("Portée n°: $rowIdx: && rs ${rows.size}")
@ -1023,7 +1098,6 @@ private suspend fun generatePdfToBytes(
// Point de départ des notes (en tenant compte des marqueurs)
val notesStartY = if (rowHasMarkers) y - markerHeight else y
val pitchPattern = "\\b(d|di|r|ri|m|mi|f|fi|s|si|l|la|t|ta)\\b".toRegex()
fun singingVoiceIndices(tuo: TimeUnitObject): List<Int> {
return tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line ->
val clean = line.trim()