From 6a0c3e4e0cfcd0b17cef79058ec8171941cc6620 Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Thu, 17 Sep 2026 15:26:26 +0300 Subject: [PATCH] Sync Column count with measure(multiple) on print --- .../kotlin/mg/dot/feufaro/pdf/PdfExport.kt | 36 ++++-- .../kotlin/mg/dot/feufaro/pdf/PdfExport.kt | 112 +++++++++++++++--- 2 files changed, 121 insertions(+), 27 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt index 619cfea..605abe6 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt @@ -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): 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 = tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line -> @@ -1139,7 +1159,7 @@ private suspend fun generatePdfToBytes( if (dcIdx > tuoIdx) break result.append(dcText).append(" "); dcIndex++ } - result.append(")"); inDcBlock = false + result.append(") "); inDcBlock = false } } return listOf(result.toString().trim()) diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt index 895407a..fe2cf4a 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt @@ -84,34 +84,75 @@ 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 { - var totalW = 0f - text.forEach { ch -> - totalW += try { - font.getStringWidth(ch.toString().sanitize()) / 1000f * fontSize - } catch (e: Exception) { 0f } + 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 + 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 } - 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( @@ -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): 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 { return tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line -> val clean = line.trim() @@ -1677,7 +1751,7 @@ private suspend fun generatePdfToBytes( dcIndex++ } - result.append(")") + result.append(") ") // println(" 🎯 FERMETURE )") inDcBlock = false }