Add btn to Export pdf & customize print settings

This commit is contained in:
hasinarak3@gmail.com 2026-04-13 11:58:06 +03:00
parent b3d7429b59
commit 1d35b9227d
10 changed files with 1394 additions and 347 deletions

View file

@ -69,9 +69,9 @@ private fun measureNoteWidth(font: PDType0Font, fontSize: Float, text: String):
private fun bestColWidthPt( private fun bestColWidthPt(
tuoList: List<TimeUnitObject>, tuoList: List<TimeUnitObject>,
font: org.apache.pdfbox.pdmodel.font.PDType0Font, font: PDType0Font,
noteFontSize: Float, noteFontSize: Float,
lyricFont: org.apache.pdfbox.pdmodel.font.PDType0Font, lyricFont:PDType0Font,
lyricFontSize: Float lyricFontSize: Float
): Float { ): Float {
var maxNoteW = 0f var maxNoteW = 0f
@ -269,8 +269,12 @@ private fun drawNoteWithSubscript(
// ── drawHeader ──────────────────────────────────────────────────────────────── // ── drawHeader ────────────────────────────────────────────────────────────────
private fun drawHeader( private fun drawHeader(
cs: PDPageContentStream, cs: PDPageContentStream,
noteFont: PDType0Font, lyricFont: PDType0Font?, noteFont:PDType0Font,
pageWidth: Float, pageHeight: Float, marginX: Float, usableWidth: Float, lyricFont:PDType0Font?,
bolderFont: PDType0Font?,
emmentalerfFont: PDType0Font?,
pageWidth: Float, pageHeight: Float, marginX: Float, marginY: Float,
usableWidth: Float,
songTitle: String, songAuthor: String, songRhythm: String, songTitle: String, songAuthor: String, songRhythm: String,
songComposer: String, songKey: String, measure: String, songComposer: String, songKey: String, measure: String,
stanza: Int, nbStanza: Int stanza: Int, nbStanza: Int
@ -278,46 +282,100 @@ private fun drawHeader(
val titleFont = lyricFont ?: noteFont val titleFont = lyricFont ?: noteFont
val normalFont = lyricFont ?: noteFont val normalFont = lyricFont ?: noteFont
val smallFont = noteFont val smallFont = noteFont
val emmentalerFont = emmentalerfFont ?: noteFont
val boldFont = bolderFont ?: noteFont
fun tw(f: PDType0Font, sz: Float, t: String) = fun tw(f: PDType0Font, sz: Float, t: String) =
try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f } try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f }
// ── Y calculés depuis marginY au lieu de valeurs fixes ────────────────────
val titleY = pageHeight - marginY - 5f
val line2Y = pageHeight - marginY - 21f
val line3Y = pageHeight - marginY - 33f
val sepY = line3Y - 5f
// Ligne 1 — Titre centré // Ligne 1 — Titre centré
val titleSize = 15f val titleSize = 15f
val titleW = tw(titleFont, titleSize, songTitle) val titleW = tw(titleFont, titleSize, songTitle)
cs.beginText(); cs.setFont(titleFont, titleSize) cs.beginText(); cs.setFont(titleFont, titleSize)
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, pageHeight - 48f) cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, titleY)
cs.showText(songTitle.sanitize()); cs.endText() cs.showText(songTitle.sanitize())
cs.endText()
// Ligne 2 — Auteur | Compositeur // Ligne 2 — Author | Composer (droite)
val line2Y = pageHeight - 64f; val sz2 = 9f val sz2 = 9f
cs.beginText(); cs.setFont(normalFont, sz2) cs.beginText(); cs.setFont(normalFont, sz2)
cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText() cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText()
val composerW = tw(normalFont, sz2, songComposer) val composerW = tw(normalFont, sz2, songComposer)
cs.beginText(); cs.setFont(normalFont, sz2) cs.beginText();
cs.setFont(normalFont, sz2)
cs.newLineAtOffset(marginX + usableWidth - composerW, line2Y) cs.newLineAtOffset(marginX + usableWidth - composerW, line2Y)
cs.showText(songComposer.sanitize()); cs.endText() cs.showText(songComposer.sanitize());
cs.endText()
// Ligne 3 — Key | Measure | Rythme // Ligne 3 — Key | Measure | Rhythm
val line3Y = pageHeight - 76f; val sz3 = 8f val sz3 = 10f
val keyTxt = "Dô dia ${songKey.sanitize()}" val keyTxt = "Dô dia "
cs.beginText(); cs.setFont(smallFont, sz3) cs.beginText();
cs.newLineAtOffset(marginX, line3Y); cs.showText(keyTxt); cs.endText() cs.setFont(smallFont, sz3)
cs.newLineAtOffset(marginX, line3Y);
cs.showText(keyTxt);
cs.endText()
val keyW = tw(smallFont, sz3, keyTxt) val keyW_2 = tw(smallFont, sz3, keyTxt)
cs.beginText(); cs.setFont(smallFont, sz3) val keyTxt2 = "${songKey.sanitize()}"
cs.newLineAtOffset(marginX + keyW + 10f, line3Y) cs.beginText(); cs.setFont(boldFont, sz3+2f)
cs.showText(measure.sanitize()); cs.endText() cs.newLineAtOffset(marginX + keyW_2 + 3f, line3Y);
cs.showText(keyTxt2);
cs.endText()
val keyW = tw(boldFont, sz3, keyTxt+keyTxt2) + 10f
val sz32 = 14f
val splitMeaRgx = """(\d+/\d+)\s+(.*)""".toRegex()
val matchRes = splitMeaRgx.matchEntire(measure)
var timeSignature = ""
var titleSignature = ""
if(matchRes != null) {
timeSignature = matchRes.groupValues[1]
titleSignature = matchRes.groupValues[2]
}
val timesig = timeSignature.toCharArray()
var ksW = keyW
timesig.forEach { c ->
cs.beginText();
if(c == '/') {
cs.setFont(normalFont, sz32)
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
ksW += tw(normalFont, sz32, c.toString())
} else {
cs.setFont(emmentalerFont, sz32)
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
ksW += tw(emmentalerFont, sz32, c.toString())
}
cs.showText(c.toString());
cs.endText()
}
cs.beginText();
cs.setFont(smallFont, sz3)
cs.newLineAtOffset(marginX + ksW + 15f, line3Y)
cs.showText("${titleSignature}");
cs.endText()
val rhythmW = tw(normalFont, sz3, songRhythm.sanitize()) val rhythmW = tw(normalFont, sz3, songRhythm.sanitize())
cs.beginText(); cs.setFont(normalFont, sz2) cs.beginText();
cs.setFont(normalFont, sz2)
cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y) cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y)
cs.showText(songRhythm.sanitize()); cs.endText() cs.showText(songRhythm.sanitize());
cs.endText()
// Ligne de séparation // Ligne de séparation
cs.setStrokingColor(0.6f, 0.6f, 0.6f); cs.setLineWidth(0.4f) cs.setStrokingColor(0.6f, 0.6f, 0.6f);
cs.moveTo(marginX, line3Y - 5f); cs.lineTo(marginX + usableWidth, line3Y - 5f); cs.stroke() cs.setLineWidth(0.4f)
cs.moveTo(marginX, sepY);
cs.lineTo(marginX + usableWidth, sepY);
cs.stroke()
} }
// ── actual fun ──────────────────────────────────────────────────────────────── // ── actual fun ────────────────────────────────────────────────────────────────
@ -333,8 +391,8 @@ actual fun rememberPdfExportAction(
songAut: String, songAut: String,
songComp: String, songComp: String,
songRythm: String songRythm: String
): () -> Unit { ): (PrintSettings) -> Unit {
return { return { settings: PrintSettings ->
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
val (pdfBytes, computedFileName) = generatePdfToBytes( val (pdfBytes, computedFileName) = generatePdfToBytes(
gridData, songTitle, measure, stanza, gridData, songTitle, measure, stanza,
@ -360,7 +418,8 @@ private suspend fun generatePdfToBytes(
songKey: String, songKey: String,
songAut: String, songAut: String,
songComp: String, songComp: String,
songRythm: String songRythm: String,
settings: PrintSettings = defaultPrintSettings()
): Pair<ByteArray, String> { ): Pair<ByteArray, String> {
val tuoList = gridData.tuoList.drop(1) val tuoList = gridData.tuoList.drop(1)
@ -370,20 +429,21 @@ private suspend fun generatePdfToBytes(
fun getVoicesFromPrefix(syl: String): List<Int> = fun getVoicesFromPrefix(syl: String): List<Int> =
REGEX_PREFIX_DIGITS.findAll(syl).map { it.groupValues[1].toInt() }.toList() REGEX_PREFIX_DIGITS.findAll(syl).map { it.groupValues[1].toInt() }.toList()
val markerHeight = 16f val markerHeight = settings.markerHeight
val noteFontSize = 12.5f val noteFontSize = settings.noteFontSize
val lyricFontSize = 13.25f val lyricFontSize = settings.lyricFontSize
val noteLineH = 15f val noteLineH = settings.noteLineH
val lyricLineH = 13f val lyricLineH = settings.lyricLineH
val extraSylLineH = 14f val extraSylLineH = settings.extraSylLineH
val adjustedMargin = 14f val adjustedMargin = settings.adjustedMargin
val interRowGap = 16f val interRowGap = settings.interRowGap
val marginX = 40f val marginX = settings.marginX
val marginY = 50f val marginY = settings.marginY
val headerH = 55f val headerH = settings.headerH
val pageWidth = PDRectangle.A4.width val pageWidth = settings.pageWidth
val pageHeight = PDRectangle.A4.height val pageHeight = settings.pageHeight
val usableWidth = pageWidth - 2 * marginX val usableWidth = pageWidth - 2 * marginX
val customNbGrid = settings.nbGridOnMeasure
// ── Polices ──────────────────────────────────────────────────────────────── // ── Polices ────────────────────────────────────────────────────────────────
val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf") val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf")
@ -402,8 +462,12 @@ private suspend fun generatePdfToBytes(
// ── Largeur optimale de colonne ──────────────────────────────────────── // ── Largeur optimale de colonne ────────────────────────────────────────
val bestColW = bestColWidthPt(tuoList, customFont, noteFontSize, lyricFont, lyricFontSize) val bestColW = bestColWidthPt(tuoList, customFont, noteFontSize, lyricFont, lyricFontSize)
val colCount = ((usableWidth / bestColW).toInt()).coerceAtLeast(1) val colCount = if (customNbGrid == 0) {
val colWidth = bestColW ((usableWidth / bestColW).toInt()).coerceAtLeast(1)
} else {
customNbGrid
}
val colWidth = usableWidth / colCount
val maxSylsSize = tuoList.maxOfOrNull { tuo -> val maxSylsSize = tuoList.maxOfOrNull { tuo ->
(1..nbStanza).maxOfOrNull { sz -> tuo.getSingleSyllable(sz).size } ?: 1 (1..nbStanza).maxOfOrNull { sz -> tuo.getSingleSyllable(sz).size } ?: 1
@ -411,7 +475,7 @@ private suspend fun generatePdfToBytes(
// ── Fonctions de dessin locales ──────────────────────────────────────── // ── Fonctions de dessin locales ────────────────────────────────────────
fun newPage(): PDPageContentStream { fun newPage(): PDPageContentStream {
val page = PDPage(PDRectangle.A4) val page = PDPage(settings.pdRectangle)
doc.addPage(page) doc.addPage(page)
return PDPageContentStream(doc, page) return PDPageContentStream(doc, page)
} }
@ -451,27 +515,78 @@ private suspend fun generatePdfToBytes(
cs.restoreGraphicsState() cs.restoreGraphicsState()
} }
// ── Fermata (dessin vectoriel — remplace le rendu glyphe Emmentaler) ── fun drawFermata(cs: PDPageContentStream, x: Float, y: Float, scale: Float = 1f) {
fun drawFermata(cs: PDPageContentStream, x: Float, y: Float, width: Float = 10f, height: Float = 12f) { fun px(svgX: Float) = x + svgX * scale
cs.setStrokingColor(0f, 0f, 0f); cs.setNonStrokingColor(0f, 0f, 0f) fun py(svgY: Float) = y + svgY * scale
val circleRadius = width * 0.22f
val circleX = x + width / 2 fun curveTo(
val circleY = y + height * 0.25f cp1x: Float, cp1y: Float,
val k = 0.5522847498f cp2x: Float, cp2y: Float,
cs.moveTo(circleX + circleRadius, circleY) ex: Float, ey: Float
cs.curveTo(circleX + circleRadius, circleY + circleRadius * k, circleX + circleRadius * k, circleY + circleRadius, circleX, circleY + circleRadius) ) = cs.curveTo(px(cp1x), py(cp1y), px(cp2x), py(cp2y), px(ex), py(ey))
cs.curveTo(circleX - circleRadius * k, circleY + circleRadius, circleX - circleRadius, circleY + circleRadius * k, circleX - circleRadius, circleY)
cs.curveTo(circleX - circleRadius, circleY - circleRadius * k, circleX - circleRadius * k, circleY - circleRadius, circleX, circleY - circleRadius) fun drawEllipse(cx: Float, cy: Float, rx: Float, ry: Float) {
cs.curveTo(circleX + circleRadius * k, circleY - circleRadius, circleX + circleRadius, circleY - circleRadius * k, circleX + circleRadius, circleY) val k = 0.5523f
val left = cx - rx; val right = cx + rx
val bottom = cy - ry; val top = cy + ry
cs.moveTo(px(right), py(cy))
// quart haut-droit
cs.curveTo(
px(right), py(cy + ry * k),
px(cx + rx * k), py(top),
px(cx), py(top)
)
// quart haut-gauche
cs.curveTo(
px(cx - rx * k), py(top),
px(left), py(cy + ry * k),
px(left), py(cy)
)
// quart bas-gauche
cs.curveTo(
px(left), py(cy - ry * k),
px(cx - rx * k), py(bottom),
px(cx), py(bottom)
)
// quart bas-droit
cs.curveTo(
px(cx + rx * k), py(bottom),
px(right), py(cy - ry * k),
px(right), py(cy)
)
cs.closePath()
}
cs.setNonStrokingColor(0f, 0f, 0f) // RGB noir
cs.moveTo(px(0.3034f), py(0.6922f))
// Dôme extérieur — côté gauche
curveTo(2.6605f, 16.6051f, 13.8570f, 30.7498f, 28.0000f, 30.7498f)
// Dôme extérieur — côté droit
curveTo(42.1430f, 30.7498f, 53.3395f, 16.6051f, 55.6966f, 0.6922f)
// Épaisseur bord droit
curveTo(55.8145f, 0.1028f, 52.8680f, 0.1028f, 52.7502f, 0.6922f)
// Dôme intérieur — côté droit
curveTo(49.8037f, 11.8901f, 42.1430f, 22.4987f, 28.0000f, 22.4987f)
// Dôme intérieur — côté gauche
curveTo(13.8570f, 22.4987f, 6.1963f, 11.8901f, 3.2498f, 0.6922f)
// Épaisseur bord gauche
curveTo(3.1320f, 0.1028f, 0.1855f, 0.1028f, 0.3034f, 0.6922f)
cs.closePath()
cs.fill()
drawEllipse(
cx = 28.0000f,
cy = 5.8492f,
rx = 5.5983f,
ry = 5.5990f
)
cs.fill() cs.fill()
cs.setLineWidth(1.5f)
val arcStartX = x + width * 0.2f; val arcStartY = y + height * 0.55f
val arcPeakX = x + width / 2; val arcPeakY = y + height * 0.85f
val arcEndX = x + width * 0.8f; val arcEndY = y + height * 0.55f
cs.moveTo(arcStartX, arcStartY)
cs.curveTo(arcStartX + width * 0.15f, arcPeakY, arcPeakX - width * 0.15f, arcPeakY, arcPeakX, arcPeakY)
cs.curveTo(arcPeakX + width * 0.15f, arcPeakY, arcEndX - width * 0.15f, arcStartY, arcEndX, arcStartY)
cs.stroke()
} }
// ── drawMarkers ──────────────────────────────────────────────────────── // ── drawMarkers ────────────────────────────────────────────────────────
@ -493,7 +608,7 @@ private suspend fun generatePdfToBytes(
// Point d'orgue → drawFermata (pas de GeneralPath sur Android) // Point d'orgue → drawFermata (pas de GeneralPath sur Android)
if (markerText.contains("𝄐") || tuo.pTemplate.template.contains("𝄐")) { if (markerText.contains("𝄐") || tuo.pTemplate.template.contains("𝄐")) {
drawFermata(cs, colX + colWidth / 2 - 5f, markerY - 2f, 10f, 12f) drawFermata(cs, colX + colWidth / 2 - 5f, markerY - 2f, 0.3f)
} }
// Marqueur texte // Marqueur texte
@ -540,8 +655,8 @@ private suspend fun generatePdfToBytes(
var y = pageHeight - marginY - headerH var y = pageHeight - marginY - headerH
drawHeader( drawHeader(
cs, customFont, lyricFont, cs, customFont, lyricFont, markerBoldFont, markerEmmetFont,
pageWidth, pageHeight, marginX, usableWidth, pageWidth, pageHeight, marginX, marginY, usableWidth,
songTitle = songTitle, songTitle = songTitle,
songAuthor = songAut, songAuthor = songAut,
songRhythm = songRythm, songRhythm = songRythm,
@ -812,7 +927,7 @@ private suspend fun generatePdfToBytes(
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) || val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
cleanSyl0.all { it == '―' || it == '-' || it == '—' } cleanSyl0.all { it == '―' || it == '-' || it == '—' }
val isAllZeroSpacing = customSpacings.all { it == 0 } // val isAllZeroSpacing = customSpacings.all { it == 0 }
if (prefixes.contains(1) && isAllZeroSpacing && lignePrincipaleEstLibre) { if (prefixes.contains(1) && isAllZeroSpacing && lignePrincipaleEstLibre) {
// FORCE LE DESSIN SUR LA LIGNE PRINCIPALE (SYL0) // FORCE LE DESSIN SUR LA LIGNE PRINCIPALE (SYL0)

View file

@ -0,0 +1,51 @@
package mg.dot.feufaro.pdf
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
actual data class PrintSettings actual constructor(
actual val pageSize : PageSize,
actual val orientation : PageOrientation,
actual val markerHeight : Float,
actual val noteFontSize : Float,
actual val lyricFontSize : Float,
actual val noteLineH : Float,
actual val lyricLineH : Float,
actual val extraSylLineH : Float,
actual val adjustedMargin : Float,
actual val interRowGap : Float,
actual val marginX : Float,
actual val marginY : Float,
actual val headerH : Float,
actual val nbGridOnMeasure: Int
) {
val pdRectangle: PDRectangle
get() {
val base = when (pageSize) {
PageSize.A4 -> PDRectangle.A4
PageSize.A3 -> PDRectangle.A3
PageSize.LETTER -> PDRectangle.LETTER
}
return if (orientation == PageOrientation.PORTRAIT) base
else PDRectangle(base.height, base.width)
}
actual val pageWidth : Float get() = pdRectangle.width
actual val pageHeight : Float get() = pdRectangle.height
}
actual fun defaultPrintSettings(): PrintSettings = PrintSettings(
pageSize = PageSize.A4,
orientation = PageOrientation.PORTRAIT,
markerHeight = 16f,
noteFontSize = 12f,
lyricFontSize = 12.5f,
noteLineH = 17f,
lyricLineH = 15f,
extraSylLineH = 15f,
adjustedMargin = 14f,
interRowGap = 22f,
marginX = 42.5f,
marginY = 42.5f,
headerH = 55f,
nbGridOnMeasure = 0
)

View file

@ -16,4 +16,4 @@ expect fun rememberPdfExportAction(
songAut: String, songAut: String,
songComp: String, songComp: String,
songRythm: String songRythm: String
): () -> Unit ): (PrintSettings) -> Unit

View file

@ -0,0 +1,48 @@
package mg.dot.feufaro.pdf
enum class PageSize(val label: String) {
A4("A4"),
A3("A3"),
LETTER("Letter")
}
enum class PageOrientation(val label: String) {
PORTRAIT("Portrait"),
LANDSCAPE("Paysage")
}
expect class PrintSettings(
pageSize : PageSize,
orientation : PageOrientation,
markerHeight : Float,
noteFontSize : Float,
lyricFontSize : Float,
noteLineH : Float,
lyricLineH : Float,
extraSylLineH : Float,
adjustedMargin : Float,
interRowGap : Float,
marginX : Float,
marginY : Float,
headerH : Float,
nbGridOnMeasure : Int
) {
val pageSize : PageSize
val orientation : PageOrientation
val markerHeight : Float
val noteFontSize : Float
val lyricFontSize : Float
val noteLineH : Float
val lyricLineH : Float
val extraSylLineH : Float
val adjustedMargin : Float
val interRowGap : Float
val marginX : Float
val marginY : Float
val headerH : Float
val nbGridOnMeasure : Int
val pageWidth : Float
val pageHeight : Float
}
expect fun defaultPrintSettings(): PrintSettings

View file

@ -560,7 +560,9 @@ fun LazyVerticalGridTUO(
val itemMinBaseWidth = bestTUOWidth(tuoList) val itemMinBaseWidth = bestTUOWidth(tuoList)
val gridWidthDp = with(density) { gridWidthPx.toDp() } val gridWidthDp = with(density) { gridWidthPx.toDp() }
val gridColumnCount: Int = remember(gridWidthDp, itemMinBaseWidth, columnGroup) { val gridCount by sharedScreenModel.gridCount.collectAsState()
var gridColumnCount: Int = remember(gridWidthDp, itemMinBaseWidth, columnGroup) {
if (gridWidthDp == 0.dp) return@remember columnGroup if (gridWidthDp == 0.dp) return@remember columnGroup
var calculatedCols = var calculatedCols =
(gridWidthDp / (itemMinBaseWidth + horizontalArrangementSpacing)).toInt() (gridWidthDp / (itemMinBaseWidth + horizontalArrangementSpacing)).toInt()
@ -572,6 +574,7 @@ fun LazyVerticalGridTUO(
val actualColumnCount: Int = calculatedCols.coerceAtLeast(columnGroup) val actualColumnCount: Int = calculatedCols.coerceAtLeast(columnGroup)
actualColumnCount actualColumnCount
} }
gridColumnCount += gridCount
val flowRowSize: Float = 0.98f / gridColumnCount val flowRowSize: Float = 0.98f / gridColumnCount
val currentStanza = viewModel.stanza val currentStanza = viewModel.stanza

View file

@ -18,14 +18,17 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mg.dot.feufaro.data.getDrawerItems import mg.dot.feufaro.data.getDrawerItems
import mg.dot.feufaro.pdf.PrintSettings
import mg.dot.feufaro.pdf.defaultPrintSettings
import mg.dot.feufaro.ui.PrintSettingsDialog
import mg.dot.feufaro.pdf.rememberPdfExportAction import mg.dot.feufaro.pdf.rememberPdfExportAction
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.viewmodel.SolfaScreenModel import mg.dot.feufaro.viewmodel.SolfaScreenModel
@ -111,7 +114,10 @@ LaunchedEffect(isPlay, isPos) {
) )
}, content = { }, content = {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val pdfExportAction: () -> Unit = rememberPdfExportAction( var showPrintSettings by remember { mutableStateOf(false) }
var pendingPrintSettings by remember { mutableStateOf<PrintSettings?>(null) }
val pdfExportAction: (PrintSettings) -> Unit = rememberPdfExportAction(
scope = scope, scope = scope,
fileRepository = solfaScreenModel.fileRepository, fileRepository = solfaScreenModel.fileRepository,
gridData = sharedScreenModel.currentGridData, gridData = sharedScreenModel.currentGridData,
@ -124,6 +130,17 @@ LaunchedEffect(isPlay, isPos) {
songComp = songComposer, songComp = songComposer,
songRythm = songRhythm songRythm = songRhythm
) )
if (showPrintSettings) {
PrintSettingsDialog(
initialSettings = defaultPrintSettings(),
onDismiss = { showPrintSettings = false },
onConfirm = { settings ->
showPrintSettings = false
pdfExportAction(settings)
}
)
}
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
TopAppBar( TopAppBar(
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = { modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
@ -171,6 +188,42 @@ LaunchedEffect(isPlay, isPos) {
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End, modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(7.dp) verticalArrangement = Arrangement.spacedBy(7.dp)
) { ) {
AnimatedVisibility(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
Row {
Column {
FloatingActionButton(
onClick = {
sharedScreenModel.descGridCount(1)
}, modifier = Modifier.size(30.dp).alpha(0.45f)
) {
Icon(
Icons.Default.Remove,
contentDescription = null,
tint = Color.Blue
)
}
}
Column {
FloatingActionButton(
onClick = {
sharedScreenModel.addGridCount(1)
}, modifier = Modifier.size(30.dp).alpha(0.45f)
) {
Icon(
Icons.Default.Add,
contentDescription = null,
tint = Color.Blue
)
}
}
}
}
AnimatedVisibility( AnimatedVisibility(
visible = isExpanded and !showMidiCtrl, visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
@ -198,7 +251,7 @@ LaunchedEffect(isPlay, isPos) {
) { ) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
pdfExportAction() showPrintSettings = !showPrintSettings
}, modifier = Modifier.alpha(0.45f) }, modifier = Modifier.alpha(0.45f)
) { ) {
Icon( Icon(

View file

@ -0,0 +1,653 @@
package mg.dot.feufaro.ui
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.PictureAsPdf
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.PathParser
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import mg.dot.feufaro.pdf.PageOrientation
import mg.dot.feufaro.pdf.PageSize
import mg.dot.feufaro.pdf.PrintSettings
import mg.dot.feufaro.pdf.defaultPrintSettings
import kotlin.math.roundToInt
@Composable
fun PrintSettingsDialog(
initialSettings: PrintSettings = defaultPrintSettings(),
onDismiss: () -> Unit,
onConfirm: (PrintSettings) -> Unit,
) {
// ── État local (copie mutable des paramètres) ─────────────────────────────
var pageSize by remember { mutableStateOf(initialSettings.pageSize) }
var orientation by remember { mutableStateOf(initialSettings.orientation) }
var noteFontSize by remember { mutableStateOf(initialSettings.noteFontSize) }
var lyricFontSize by remember { mutableStateOf(initialSettings.lyricFontSize) }
var markerHeight by remember { mutableStateOf(initialSettings.markerHeight) }
var noteLineH by remember { mutableStateOf(initialSettings.noteLineH) }
var lyricLineH by remember { mutableStateOf(initialSettings.lyricLineH) }
var extraSylLineH by remember { mutableStateOf(initialSettings.extraSylLineH) }
var adjustedMargin by remember { mutableStateOf(initialSettings.adjustedMargin) }
var interRowGap by remember { mutableStateOf(initialSettings.interRowGap) }
var marginX by remember { mutableStateOf(initialSettings.marginX) }
var marginY by remember { mutableStateOf(initialSettings.marginY) }
var headerH by remember { mutableStateOf(initialSettings.headerH) }
var nbGridOnMeasure by remember { mutableStateOf(initialSettings.nbGridOnMeasure) }
Dialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier
.fillMaxWidth(0.92f)
.fillMaxHeight(0.92f),
shape = RoundedCornerShape(16.dp),
tonalElevation = 8.dp,
color = MaterialTheme.colorScheme.surface
) {
Column(modifier = Modifier.fillMaxSize()) {
// ── Barre de titre ────────────────────────────────────────────
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primary)
.padding(horizontal = 20.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Paramètres d'impression",
color = MaterialTheme.colorScheme.onPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
IconButton(onClick = onDismiss) {
Icon(
Icons.Filled.Close,
contentDescription = "Fermer",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
// ── Corps scrollable en deux colonnes ─────────────────────────
Row(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
// ── Colonne gauche : Format papier + Preview ──────────────
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
SectionTitle("Format de page")
Row{
Column(
modifier = Modifier.fillMaxWidth(0.7f)
) {
LabelRow("Nombre de colonne d'une portée")
}
Column {
TextField(
value = if (nbGridOnMeasure == 0) "" else nbGridOnMeasure.toString(),
onValueChange = { newValue ->
val number = newValue.toIntOrNull()?.coerceAtMost(25)
if (number != null) {
nbGridOnMeasure = number
} else if (newValue.isEmpty()) {
nbGridOnMeasure = 0
}
},
modifier = Modifier.height(50.dp)
)
}
}
Spacer(Modifier.height(4.dp))
// Taille papier
LabelRow("Taille du papier")
SegmentedRow(
options = PageSize.entries.map { it.label },
selected = pageSize.ordinal,
onSelect = { pageSize = PageSize.entries[it] }
)
Spacer(Modifier.height(4.dp))
// Orientation
LabelRow("Orientation")
SegmentedRow(
options = PageOrientation.entries.map { it.label },
selected = orientation.ordinal,
onSelect = { orientation = PageOrientation.entries[it] }
)
Spacer(Modifier.height(8.dp))
// ── Preview miniature de la page ──────────────────────
SectionTitle("Aperçu")
val currentSettings = PrintSettings(
pageSize = pageSize,
orientation = orientation,
noteFontSize = noteFontSize,
lyricFontSize = lyricFontSize,
markerHeight = markerHeight,
noteLineH = noteLineH,
lyricLineH = lyricLineH,
extraSylLineH = extraSylLineH,
adjustedMargin = adjustedMargin,
interRowGap = interRowGap,
marginX = marginX,
marginY = marginY,
headerH = headerH,
nbGridOnMeasure = nbGridOnMeasure
)
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
PagePreview(
settings = currentSettings,
modifier = Modifier
.height(250.dp) // Hauteur fixe demandée
.padding(vertical = 8.dp)
)
}
}
// ── Colonne droite : Paramètres typographiques / marges ───
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
SectionTitle("Typographie")
FloatSlider(
label = "Taille fonte notes",
value = noteFontSize,
range = 8f..18f,
unit = "pt"
) { noteFontSize = it }
FloatSlider(
label = "Taille fonte paroles",
value = lyricFontSize,
range = 8f..18f,
unit = "pt"
) { lyricFontSize = it }
Spacer(Modifier.height(4.dp))
SectionTitle("Espacements")
FloatSlider(
label = "Hauteur zone marqueurs",
value = markerHeight,
range = 8f..32f,
unit = "mm"
) { markerHeight = it }
FloatSlider(
label = "Espace entre voix",
value = noteLineH,
range = 10f..34f,
unit = "mm"
) { noteLineH = it }
FloatSlider(
label = "Espace lignes paroles restantes",
value = lyricLineH,
range = 10f..30f,
unit = "mm"
) { lyricLineH = it }
FloatSlider(
label = "Espace syl0 / syl1+",
value = extraSylLineH,
range = 8f..30f,
unit = "mm"
) { extraSylLineH = it }
FloatSlider(
label = "Marge ajustée syl0",
value = adjustedMargin,
range = 4f..30f,
unit = "mm"
) { adjustedMargin = it }
FloatSlider(
label = "Écart entre portées",
value = interRowGap,
range = 10f..60f,
unit = "mm"
) { interRowGap = it }
Spacer(Modifier.height(4.dp))
SectionTitle("Marges")
FloatSlider(
label = "Marge gauche/droite",
value = marginX,
range = 15f..80f,
unit = "mm"
) { marginX = it }
FloatSlider(
label = "Marge haut/bas",
value = marginY,
range = 15f..80f,
unit = "mm"
) { marginY = it }
FloatSlider(
label = "Hauteur en-tête",
value = headerH,
range = 49f..90f,
unit = "mm"
) { headerH = it }
}
}
// ── Boutons bas ───────────────────────────────────────────────
HorizontalDivider()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
verticalAlignment = Alignment.CenterVertically
) {
// Réinitialiser
TextButton(onClick = {
val def = defaultPrintSettings()
pageSize = def.pageSize
orientation = def.orientation
noteFontSize = def.noteFontSize
lyricFontSize = def.lyricFontSize
markerHeight = def.markerHeight
noteLineH = def.noteLineH
lyricLineH = def.lyricLineH
extraSylLineH = def.extraSylLineH
adjustedMargin = def.adjustedMargin
interRowGap = def.interRowGap
marginX = def.marginX
marginY = def.marginY
headerH = def.headerH
}) {
Icon(
imageVector = Icons.Default.RestartAlt,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
// Spacer(Modifier.weight(1f))
OutlinedButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
Button(onClick = {
onConfirm(
PrintSettings(
pageSize = pageSize,
orientation = orientation,
noteFontSize = noteFontSize,
lyricFontSize = lyricFontSize,
markerHeight = markerHeight,
noteLineH = noteLineH,
lyricLineH = lyricLineH,
extraSylLineH = extraSylLineH,
adjustedMargin = adjustedMargin,
interRowGap = interRowGap,
marginX = marginX,
marginY = marginY,
headerH = headerH,
nbGridOnMeasure = nbGridOnMeasure
)
)
}) {
Icon(
imageVector = Icons.Default.PictureAsPdf,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
}
}
}
}
}
// ── Composants internes ───────────────────────────────────────────────────────
@Composable
private fun SectionTitle(text: String) {
Text(
text = text,
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 4.dp)
)
HorizontalDivider(
modifier = Modifier.padding(bottom = 4.dp),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
)
}
@Composable
private fun LabelRow(text: String) {
Text(
text = text,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@Composable
private fun SegmentedRow(
options: List<String>,
selected: Int,
onSelect: (Int) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline,
shape = RoundedCornerShape(8.dp)
)
) {
options.forEachIndexed { index, label ->
val isSelected = index == selected
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.weight(1f)
.background(
if (isSelected) MaterialTheme.colorScheme.primary
else Color.Transparent
)
.clickable { onSelect(index) }
.padding(vertical = 8.dp)
) {
Text(
text = label,
fontSize = 13.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onSurface
)
}
}
}
}
/**
* Slider flottant avec label + valeur affichée.
*/
fun Float.toMm(): Float {
return this * 0.3528f
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun FloatSlider(
label: String,
value: Float,
range: ClosedFloatingPointRange<Float>,
unit: String,
onValueChange: (Float) -> Unit
) {
val displayValue = if (unit == "mm") value.toMm() else value
val roundedDisplayValue = (displayValue * 2).roundToInt() / 2f
val primaryTeal = Color(0xFF009688)
val lightTeal = Color(0xFFB2DFDB)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
// 1. Label de description fixe à gauche
Text(
text = label,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// 2. Zone du Slider avec la valeur flottante au-dessus
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val widthPx = constraints.maxWidth.toFloat()
// Calcul de la position X du texte pour suivre le thumb
// On normalise la valeur entre 0.0 et 1.0
val fraction = (value - range.start) / (range.endInclusive - range.start)
val textOffset = (widthPx * fraction)
// Affichage de la valeur qui "flotte" au-dessus du thumb
Box(
modifier = Modifier
.fillMaxWidth()
.height(20.dp) // Espace pour le texte
) {
Text(
text = "$roundedDisplayValue $unit",
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = primaryTeal,
modifier = Modifier.graphicsLayer() {
translationX = textOffset - (30.dp.toPx()) // Ajustement pour centrer le texte
}
)
}
Slider(
value = value,
onValueChange = { onValueChange((it * 2).roundToInt() / 2f) },
valueRange = range,
steps = (((range.endInclusive - range.start) / 0.5f).toInt() - 1).coerceAtLeast(0),
modifier = Modifier.fillMaxWidth().padding(top = 15.dp), // Décale le slider sous le texte
thumb = {
Box(
modifier = Modifier
.size(15.dp)
.background(primaryTeal, CircleShape)
)
},
track = { sliderState ->
SliderDefaults.Track(
sliderState = sliderState,
modifier = Modifier.height(8.dp),
colors = SliderDefaults.colors(
activeTrackColor = primaryTeal,
inactiveTrackColor = lightTeal
),
drawStopIndicator = null
)
}
)
}
}
}
/**
* Mini preview de la page
*/
@Composable
private fun PagePreview(
settings: PrintSettings,
modifier: Modifier = Modifier
) {
val isLandscape = settings.orientation == PageOrientation.LANDSCAPE
val ratio = if (isLandscape) 1.414f else 0.707f
val baseScale = 200f / 595f // Échelle pour l'affichage
Box(
modifier = modifier
.aspectRatio(ratio)
.clip(RoundedCornerShape(4.dp))
.background(Color.White)
.border(1.dp, Color.Gray.copy(alpha = 0.3f), RoundedCornerShape(4.dp)),
contentAlignment = Alignment.TopStart
) {
// Conversion des unités en DP
val mx = (settings.marginX * baseScale).dp
val my = (settings.marginY * baseScale).dp
val hH = (settings.headerH * baseScale).dp
val nH = (settings.noteLineH * baseScale).dp // Espace entre 2 voix
val iG = (settings.interRowGap * baseScale).dp // Entre portées
val mH = (settings.markerHeight * baseScale).dp // Zone orange
val aM = (settings.adjustedMargin * baseScale).dp // Marge syl0
val eS = (settings.extraSylLineH * baseScale).dp // Espace syl0/syl1
val pR = (settings.lyricLineH * baseScale).dp // Espace syl0/syl1
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = mx, vertical = my)
) {
// 1. HEADER
Box(
modifier = Modifier
.fillMaxWidth()
.height(hH)
.border(0.5.dp, Color.Red.copy(alpha = 0.5f))
.background(Color.Red.copy(alpha = 0.05f))
)
Spacer(modifier = Modifier.height(iG / 2))
// 2. PORTÉES
val repeatedPort = if(settings.orientation == PageOrientation.LANDSCAPE) 2 else 3
repeat(repeatedPort) {
Column(
modifier = Modifier
.fillMaxWidth()
.border(0.5.dp, Color(0xFF2ECC71).copy(alpha = 0.4f)) // Cadre Vert
.padding(2.dp)
) {
// Ligne des Marqueurs
Box(
modifier = Modifier
.fillMaxWidth()
.height(mH / 2)
.border(0.5.dp, Color(0xFFE67E22)) // Orange
.background(Color(0xFFE67E22).copy(alpha = 0.1f))
)
// Lignes des Notes par voix
Row(verticalAlignment = Alignment.CenterVertically) {
AccoladeShape(modifier = Modifier.width(6.dp).height(nH * 2.5f))
Column(modifier = Modifier.weight(1f).padding(start = 2.dp)) {
repeat(4) {
Box(
modifier = Modifier
.fillMaxWidth(0.9f)
.height(nH / 3)
.border(0.3.dp, Color(0xFF00BCD4)) // Bleu Ciel
.background(Color(0xFF00BCD4).copy(alpha = 0.1f))
)
Spacer(modifier = Modifier.height(nH / 6))
}
}
}
// Paroles Syl0 et Syl1
Spacer(modifier = Modifier.height(aM / 2))
Column(
modifier = Modifier
.fillMaxWidth(0.85f)
.border(0.5.dp, Color(0xFF9B59B6)) // Violet
.padding(1.dp)
) {
// Syl0
Box(modifier = Modifier.fillMaxWidth().height(2.dp).background(Color(0xFF9B59B6).copy(alpha = 0.3f)))
Spacer(modifier = Modifier.height(eS / 4))
// Syl1
Box(modifier = Modifier.fillMaxWidth(0.9f).height(2.dp).background(Color(0xFF9B59B6).copy(alpha = 0.3f)))
}
}
Spacer(modifier = Modifier.height(iG))
}
// 3. PAROLES RESTANTES
Column(
modifier = Modifier
.fillMaxWidth()
.border(0.5.dp, Color.Red.copy(alpha = 0.5f))
.background(Color.Red.copy(alpha = 0.05f))
.padding(4.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
repeat(4) { index ->
Box(
modifier = Modifier
.fillMaxWidth(if (index % 2 == 0) 0.95f else 0.85f)
.height(2.dp)
.background(Color.Red.copy(alpha = 0.2f))
)
Spacer(modifier = Modifier.height(pR / 4))
}
}
}
}
}
@Composable
private fun AccoladeShape(modifier: Modifier) {
Canvas(modifier = modifier) {
// Le path que tu as fourni (normalisé sur une grille de 24x24 environ)
val pathData = "M14 21C12.8954 21 12 20.1046 12 19V15.3255C12 14.8363 12 14.5917 11.9447 14.3615C11.8957 14.1575 11.8149 13.9624 11.7053 13.7834C11.5816 13.5816 11.4086 13.4086 11.0627 13.0627L10 12L11.0627 10.9373C11.4086 10.5914 11.5816 10.4184 11.7053 10.2166C11.8149 10.0376 11.8957 9.84254 11.9447 9.63846C12 9.40829 12 9.1637 12 8.67452V5C12 3.89543 12.8954 3 14 3"
val path = PathParser().parsePathString(pathData).toPath()
val pathBounds = path.getBounds()
val scaleX = size.width / pathBounds.width
val scaleY = size.height / pathBounds.height
drawContext.canvas.save()
drawContext.canvas.scale(scaleX, scaleY)
drawContext.canvas.translate(-pathBounds.left, -pathBounds.top)
drawPath(
path = path,
color = Color.Black,
style = androidx.compose.ui.graphics.drawscope.Stroke(
width = 0.552f,
cap = androidx.compose.ui.graphics.StrokeCap.Round,
join = androidx.compose.ui.graphics.StrokeJoin.Round
)
)
drawContext.canvas.restore()
}
}

View file

@ -183,6 +183,20 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_activeIndex.value = clamped _activeIndex.value = clamped
} }
} }
private val _gridCount = MutableStateFlow(0)
val gridCount: StateFlow<Int> = _gridCount.asStateFlow()
fun addGridCount(nbGrid: Int) {
_gridCount.value += nbGrid
}
fun descGridCount(nbGrid: Int) {
if(_gridCount.value > 0) {
_gridCount.value -= nbGrid
}
}
fun resetGridCount() {
_gridCount.value = 0
}
fun seekToGrid(gridIndex: Int) { fun seekToGrid(gridIndex: Int) {
_mediaPlayer?.seekToGrid(gridIndex) _mediaPlayer?.seekToGrid(gridIndex)
} }
@ -327,6 +341,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun loadNewSong(newMidiFile: String) { fun loadNewSong(newMidiFile: String) {
_mediaPlayer?.stop() _mediaPlayer?.stop()
_mediaPlayer?.release() _mediaPlayer?.release()
_stanza.value = 1
_mediaPlayer = null _mediaPlayer = null
_isPos.value = true _isPos.value = true
_isPlay.value = false _isPlay.value = false
@ -436,6 +451,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_tuoTimestamps.value = emptyList() _tuoTimestamps.value = emptyList()
updateSearchTxt("") updateSearchTxt("")
tempTimeUnitObjectList.clear() tempTimeUnitObjectList.clear()
resetGridCount()
} }
fun lastTUO(): TimeUnitObject? { fun lastTUO(): TimeUnitObject? {
@ -464,7 +480,6 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
_stanza.value = 0 _stanza.value = 0
} }
loadNewSong("whawyd3.mid")
} }
fun setSongKey(theSongKey: String) { fun setSongKey(theSongKey: String) {

View file

@ -14,7 +14,6 @@ import org.apache.pdfbox.io.RandomAccessReadBuffer
import org.apache.pdfbox.pdmodel.PDDocument import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.pdmodel.PDPage import org.apache.pdfbox.pdmodel.PDPage
import org.apache.pdfbox.pdmodel.PDPageContentStream import org.apache.pdfbox.pdmodel.PDPageContentStream
import org.apache.pdfbox.pdmodel.common.PDRectangle
import org.apache.pdfbox.pdmodel.font.PDType0Font import org.apache.pdfbox.pdmodel.font.PDType0Font
import org.apache.pdfbox.util.Matrix import org.apache.pdfbox.util.Matrix
import java.awt.Color import java.awt.Color
@ -388,7 +387,6 @@ private fun drawTUOUnderlines(
} }
// ── Y ── identique à Compose : voiceNumber * totalH / nbNotes → bas de la ligne de voix // ── Y ── identique à Compose : voiceNumber * totalH / nbNotes → bas de la ligne de voix
// En PDFBox Y croît vers le haut donc on descend de 2f sous la baseline
// val underlineY = y - voiceLineIndex * noteLineH - 2f // val underlineY = y - voiceLineIndex * noteLineH - 2f
if (xEnd > xStart) { if (xEnd > xStart) {
@ -450,12 +448,17 @@ private fun drawNoteWithSubscript(
val ch = text[i] val ch = text[i]
val str = ch.toString() val str = ch.toString()
cs.beginText() cs.beginText()
when(ch) {
'₁', '₂', '₃', '¹', '²', '³' -> {
cs.setFont(font, fontSize * 0.6f)
} else -> {
cs.setFont(font, fontSize) cs.setFont(font, fontSize)
}
}
cs.newLineAtOffset(curX, y) cs.newLineAtOffset(curX, y)
cs.showText(str.sanitize()) cs.showText(str.sanitize())
cs.endText() cs.endText()
curX += font.getStringWidth(str.sanitize()) / 1000f * fontSize curX += font.getStringWidth(str.sanitize()) / 1000f * fontSize
// }
i++ i++
} }
} }
@ -465,7 +468,10 @@ private fun drawHeader(
cs: PDPageContentStream, cs: PDPageContentStream,
noteFont: PDType0Font, noteFont: PDType0Font,
lyricFont: PDType0Font?, lyricFont: PDType0Font?,
pageWidth: Float, pageHeight: Float, marginX: Float, usableWidth: Float, bolderFont: PDType0Font?,
emmentalerfFont: PDType0Font?,
pageWidth: Float, pageHeight: Float, marginX: Float, marginY: Float,
usableWidth: Float,
songTitle: String, songAuthor: String, songRhythm: String, songTitle: String, songAuthor: String, songRhythm: String,
songComposer: String, songKey: String, measure: String, songComposer: String, songKey: String, measure: String,
stanza: Int, nbStanza: Int stanza: Int, nbStanza: Int
@ -473,20 +479,28 @@ private fun drawHeader(
val titleFont = lyricFont ?: noteFont val titleFont = lyricFont ?: noteFont
val normalFont = lyricFont ?: noteFont val normalFont = lyricFont ?: noteFont
val smallFont = noteFont val smallFont = noteFont
val emmentalerFont = emmentalerfFont ?: noteFont
val boldFont = bolderFont ?: noteFont
fun tw(f: PDType0Font, sz: Float, t: String) = fun tw(f: PDType0Font, sz: Float, t: String) =
try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f } try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f }
// ── Y calculés depuis marginY au lieu de valeurs fixes ────────────────────
val titleY = pageHeight - marginY - 5f
val line2Y = pageHeight - marginY - 21f
val line3Y = pageHeight - marginY - 33f
val sepY = line3Y - 5f
// Ligne 1 — Titre centré // Ligne 1 — Titre centré
val titleSize = 15f val titleSize = 15f
val titleW = tw(titleFont, titleSize, songTitle) val titleW = tw(titleFont, titleSize, songTitle)
cs.beginText(); cs.setFont(titleFont, titleSize) cs.beginText(); cs.setFont(titleFont, titleSize)
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, pageHeight - 48f) cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, titleY)
cs.showText(songTitle.sanitize()); cs.showText(songTitle.sanitize())
cs.endText() cs.endText()
// Ligne 2 — Author | Composer (droite) // Ligne 2 — Author | Composer (droite)
val line2Y = pageHeight - 64f; val sz2 = 9f val sz2 = 9f
cs.beginText(); cs.setFont(normalFont, sz2) cs.beginText(); cs.setFont(normalFont, sz2)
cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText() cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText()
@ -497,35 +511,71 @@ private fun drawHeader(
cs.showText(songComposer.sanitize()); cs.showText(songComposer.sanitize());
cs.endText() cs.endText()
// Ligne 3 — Key | Measure | Rythm Stanza (droite) // Ligne 3 — Key | Measure | Rhythm
val line3Y = pageHeight - 76f; val sz3 = 8f val sz3 = 10f
val keyTxt = "Dô dia ${songKey.sanitize()}" val keyTxt = "Dô dia "
cs.beginText(); cs.beginText();
cs.setFont(smallFont, sz3) cs.setFont(smallFont, sz3)
cs.newLineAtOffset(marginX, line3Y); cs.showText(keyTxt); cs.newLineAtOffset(marginX, line3Y);
cs.showText(keyTxt);
cs.endText() cs.endText()
val keyW = tw(smallFont, sz3, keyTxt) val keyW_2 = tw(smallFont, sz3, keyTxt)
val measureTxt = "${measure.sanitize()}" val keyTxt2 = "${songKey.sanitize()}"
cs.beginText(); cs.setFont(boldFont, sz3+2f)
cs.newLineAtOffset(marginX + keyW_2 + 3f, line3Y);
cs.showText(keyTxt2);
cs.endText()
val keyW = tw(boldFont, sz3, keyTxt+keyTxt2) + 10f
val sz32 = 14f
val splitMeaRgx = """(\d+/\d+)\s+(.*)""".toRegex()
val matchRes = splitMeaRgx.matchEntire(measure)
var timeSignature = ""
var titleSignature = ""
if(matchRes != null) {
timeSignature = matchRes.groupValues[1]
titleSignature = matchRes.groupValues[2]
}
val timesig = timeSignature.toCharArray()
var ksW = keyW
timesig.forEach { c ->
cs.beginText();
if(c == '/') {
cs.setFont(normalFont, sz32)
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
ksW += tw(normalFont, sz32, c.toString())
} else {
cs.setFont(emmentalerFont, sz32)
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
ksW += tw(emmentalerFont, sz32, c.toString())
}
cs.showText(c.toString());
cs.endText()
}
cs.beginText(); cs.beginText();
cs.setFont(smallFont, sz3) cs.setFont(smallFont, sz3)
cs.newLineAtOffset(marginX + keyW + 10f, line3Y) cs.newLineAtOffset(marginX + ksW + 15f, line3Y)
cs.showText("${measure.sanitize()}"); cs.showText("${titleSignature}");
cs.endText() cs.endText()
val rhythmW = tw(normalFont, sz3, songRhythm.sanitize()) val rhythmW = tw(normalFont, sz3, songRhythm.sanitize())
cs.beginText(); cs.setFont(normalFont, sz2) cs.beginText();
cs.setFont(normalFont, sz2)
cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y) cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y)
cs.showText(songRhythm.sanitize()); cs.endText() cs.showText(songRhythm.sanitize());
cs.endText()
// Ligne de séparation // Ligne de séparation
cs.setStrokingColor(0.6f, 0.6f, 0.6f); cs.setLineWidth(0.4f) cs.setStrokingColor(0.6f, 0.6f, 0.6f);
cs.moveTo(marginX, line3Y - 5f); cs.lineTo(marginX + usableWidth, line3Y - 5f); cs.stroke() cs.setLineWidth(0.4f)
cs.moveTo(marginX, sepY);
cs.lineTo(marginX + usableWidth, sepY);
cs.stroke()
} }
actual fun rememberPdfExportAction( actual fun rememberPdfExportAction(
scope: CoroutineScope, scope: CoroutineScope,
fileRepository: FileRepository, fileRepository: FileRepository,
@ -538,13 +588,14 @@ actual fun rememberPdfExportAction(
songAut: String, songAut: String,
songComp: String, songComp: String,
songRythm: String songRythm: String
): () -> Unit { ): (PrintSettings) -> Unit {
return { return { settings: PrintSettings ->
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
// 1. Générer le PDF en mémoire (ByteArray) // 1. Générer le PDF en mémoire (ByteArray)
val (pdfBytes, computedFileName) = generatePdfToBytes( val (pdfBytes, computedFileName) = generatePdfToBytes(
gridData, songTitle, measure, stanza, gridData, songTitle, measure, stanza,
nbStanza, songKey, songAut, songComp, songRythm nbStanza, songKey, songAut, songComp, songRythm,
settings
) )
val chosenPath = withContext(Dispatchers.Main) { val chosenPath = withContext(Dispatchers.Main) {
fileRepository.pickSavePath(computedFileName) fileRepository.pickSavePath(computedFileName)
@ -565,7 +616,8 @@ private suspend fun generatePdfToBytes(
songKey: String, songKey: String,
songAut: String, songAut: String,
songComp: String, songComp: String,
songRythm: String songRythm: String,
settings: PrintSettings = defaultPrintSettings()
): Pair<ByteArray, String> { ): Pair<ByteArray, String> {
val tuoList = gridData.tuoList.drop(1) val tuoList = gridData.tuoList.drop(1)
@ -578,20 +630,21 @@ private suspend fun generatePdfToBytes(
.toList() .toList()
} }
val markerHeight = 16f val markerHeight = settings. markerHeight
val noteFontSize = 12f val noteFontSize = settings.noteFontSize
val lyricFontSize = 12.5f val lyricFontSize = settings.lyricFontSize
val noteLineH = 17f // espace entre 2 voix val noteLineH = settings.noteLineH
val lyricLineH = 15f // espace entre lignes de lyrics val lyricLineH = settings.lyricLineH
val extraSylLineH = 15f // espace entre syl0 et syl1+ (cas zero spacing) val extraSylLineH = settings.extraSylLineH
val adjustedMargin = 14f // espace entre syl0 du bloc de notes val adjustedMargin = settings.adjustedMargin
val interRowGap = 22f // espace entre deux portées val interRowGap = settings.interRowGap
val marginX = 42.5f val marginX = settings.marginX
val marginY = 42.5f val marginY = settings.marginY
val headerH = 55f // hauteur réservée au header val headerH = settings.headerH
val pageWidth = PDRectangle.A4.width val pageWidth = settings.pageWidth
val pageHeight = PDRectangle.A4.height val pageHeight = settings.pageHeight
val usableWidth = pageWidth - 2 * marginX val usableWidth = pageWidth - 2 * marginX
val customNbGrid = settings.nbGridOnMeasure
// ── Polices ──────────────────────────────────────────────────── // ── Polices ────────────────────────────────────────────────────
val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf") val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf")
@ -614,17 +667,20 @@ private suspend fun generatePdfToBytes(
) )
// ── Calcul de la largeur optimale de colonne (= bestTUOWidth) ── // ── Calcul de la largeur optimale de colonne (= bestTUOWidth) ──
val bestColW = bestColWidthPt( val bestColW = bestColWidthPt(
/*tuoList, customFont,
if (lyricFont is PDType0Font) lyricFont else customFont,
noteFontSize, lyricFontSize, stanza*/
tuoList, tuoList,
customFont, customFont,
noteFontSize, noteFontSize,
lyricFont, lyricFont,
lyricFontSize lyricFontSize
) )
val colCount = ((usableWidth / bestColW).toInt()).coerceAtLeast(1) val colCount = if (customNbGrid == 0) {
val colWidth = bestColW ((usableWidth / bestColW).toInt()).coerceAtLeast(1)
} else {
customNbGrid
}
val finalColWidth = usableWidth / colCount
//println("ici Colc est $colCount")
val colWidth = finalColWidth
val maxSylsSize = tuoList.maxOfOrNull { tuo -> // lyrics Alt val maxSylsSize = tuoList.maxOfOrNull { tuo -> // lyrics Alt
@ -635,7 +691,7 @@ private suspend fun generatePdfToBytes(
// ── Fonctions de dessin ──────────────────────────────────── // ── Fonctions de dessin ────────────────────────────────────
fun newPage(): PDPageContentStream { fun newPage(): PDPageContentStream {
val page = PDPage(PDRectangle.A4) val page = PDPage(settings.pdRectangle)
doc.addPage(page) doc.addPage(page)
val cs = PDPageContentStream(doc, page) val cs = PDPageContentStream(doc, page)
@ -832,8 +888,8 @@ private suspend fun generatePdfToBytes(
var y = pageHeight - marginY - headerH var y = pageHeight - marginY - headerH
drawHeader( drawHeader(
cs, customFont, lyricFont as? PDType0Font, cs, customFont, lyricFont as? PDType0Font, markerBoldFont, markerEmmetFont,
pageWidth, pageHeight, marginX, usableWidth, pageWidth, pageHeight, marginX, marginY, usableWidth,
songTitle = songTitle, songTitle = songTitle,
songAuthor = songAut, songAuthor = songAut,
songRhythm = songRythm, songRhythm = songRythm,
@ -844,9 +900,18 @@ private suspend fun generatePdfToBytes(
nbStanza = nbStanza nbStanza = nbStanza
) )
val rows = tuoList.chunked(colCount) 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 &&
allRows.last().size == 1 &&
allRows.last().first().noteAsMultiString().trim().isEmpty() &&
allRows.last().first().sep0 in listOf("|", "/")
val rows = if (hasTrailingBarlineRow) allRows.dropLast(1) else allRows
rows.forEachIndexed { rowIdx, rowTuos -> rows.forEachIndexed { rowIdx, rowTuos ->
// println("Portée n°: $rowIdx: && rs ${rows.size}")
val isLastVisualRow = rowIdx == rows.size - 1
val maxNoteLines = (rowTuos.maxOfOrNull { it.noteAsMultiString().split("\n").size } ?: 1) + 1 val maxNoteLines = (rowTuos.maxOfOrNull { it.noteAsMultiString().split("\n").size } ?: 1) + 1
val maxLyricLines = rowTuos.maxOfOrNull { it.getSingleSyllable(stanza).size } ?: 0 val maxLyricLines = rowTuos.maxOfOrNull { it.getSingleSyllable(stanza).size } ?: 0
@ -871,13 +936,11 @@ private suspend fun generatePdfToBytes(
// Point de départ des notes (en tenant compte des marqueurs) // Point de départ des notes (en tenant compte des marqueurs)
val notesStartY = if (rowHasMarkers) y - markerHeight else y val notesStartY = if (rowHasMarkers) y - markerHeight else y
// On regarde sur toute la ligne quelles voix (0,1,2,3) ont des notes
val pitchPattern = "\\b(d|di|r|ri|m|mi|f|fi|s|si|l|la|t|ta)\\b".toRegex() 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> { fun singingVoiceIndices(tuo: TimeUnitObject): List<Int> {
return tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line -> return tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line ->
val clean = line.trim() val clean = line.trim()
// On considère que ça "chante" si ce n'est pas vide,
// pas un simple tiret de prolongation, et que ça contient une note (do, re, mi...)
val sings = clean.isNotBlank() val sings = clean.isNotBlank()
&& !clean.all { it == '-' || it.isWhitespace() } && !clean.all { it == '-' || it.isWhitespace() }
&& clean != "" && clean != "" && clean != "" && clean != ""
@ -901,7 +964,6 @@ private suspend fun generatePdfToBytes(
val syl0EstVraimentVide = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) || val syl0EstVraimentVide = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
cleanSyl0.all { it == '―' || it == '-' || it == '—' } cleanSyl0.all { it == '―' || it == '-' || it == '—' }
// NOUVEAU : Est-ce qu'une autre ligne (syl1, syl2...) contient un préfixe "1." ?
val uneAutreVoixPrendLeSoprano = (1..4).any { vIdx -> val uneAutreVoixPrendLeSoprano = (1..4).any { vIdx ->
val s = rawSyls.getOrNull(vIdx) ?: "" val s = rawSyls.getOrNull(vIdx) ?: ""
s.contains("1.") && REGEX_HAS_LYRIC.containsMatchIn(s.replace(REGEX_CLEAN, "")) s.contains("1.") && REGEX_HAS_LYRIC.containsMatchIn(s.replace(REGEX_CLEAN, ""))
@ -924,10 +986,10 @@ private suspend fun generatePdfToBytes(
val prefixInts = REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList() val prefixInts = REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList()
if (prefixInts.isNotEmpty()) { if (prefixInts.isNotEmpty()) {
val hasTrioATB = prefixInts.size == 3 && prefixInts.containsAll(listOf(2, 3, 4))
val hasDuoSA = prefixInts.size == 2 && prefixInts.containsAll(listOf(1, 2)) val hasDuoSA = prefixInts.size == 2 && prefixInts.containsAll(listOf(1, 2))
val hasDuoAT = prefixInts.size == 2 && prefixInts.containsAll(listOf(2, 3)) val hasDuoAT = prefixInts.size == 2 && prefixInts.containsAll(listOf(2, 3))
val hasDuoTB = prefixInts.size == 2 && prefixInts.containsAll(listOf(3, 4)) val hasDuoTB = prefixInts.size == 2 && prefixInts.containsAll(listOf(3, 4))
val hasTrioATB = prefixInts.size == 3 && prefixInts.containsAll(listOf(2, 3, 4))
val isUnisson = prefixInts.containsAll(listOf(1, 2, 3, 4)) val isUnisson = prefixInts.containsAll(listOf(1, 2, 3, 4))
// Logique simplifiée pour le calcul des spacings (Duo consécutif) // Logique simplifiée pour le calcul des spacings (Duo consécutif)
@ -945,19 +1007,14 @@ private suspend fun generatePdfToBytes(
else if (hasDuoSA) { else if (hasDuoSA) {
spacings[1] = 1 spacings[1] = 1
} }
/*else if (hasDuoAT) {
spacings[1] = 0
spacings[2] = 1
}*/
else if (hasDuoTB) { else if (hasDuoTB) {
if (syl0HasRealText) { spacings[1] = 1 }
spacings[3] = 1 spacings[3] = 1
} }
else { else {
// Cas Standard (Individuel) // Cas Standard (Individuel)
prefixInts.forEach { voiceNum -> prefixInts.forEach { voiceNum ->
when (voiceNum) { when (voiceNum) {
1 -> if (syl0HasRealText) spacings[0] = 1 1 -> if (syl0HasRealText && prefixInts.contains(1)) spacings[0] = 1
2 -> if (!prefixInts.contains(1)) spacings[1] = 1 2 -> if (!prefixInts.contains(1)) spacings[1] = 1
3 -> if (!prefixInts.contains(2)) spacings[2] = 1 3 -> if (!prefixInts.contains(2)) spacings[2] = 1
4 -> if (!prefixInts.contains(3)) spacings[3] = 1 4 -> if (!prefixInts.contains(3)) spacings[3] = 1
@ -972,7 +1029,7 @@ private suspend fun generatePdfToBytes(
val customSpacings = spacings.toList() val customSpacings = spacings.toList()
/*if (rowIdx == 7 || (rowIdx == 8)) { /*if (rowIdx == 2 || (rowIdx == 2)) {
println("╠══════════════════════════════════════════════════════════════╣") println("╠══════════════════════════════════════════════════════════════╣")
println("║ customSpacings calculés : $customSpacings ") println("║ customSpacings calculés : $customSpacings ")
println("╚══════════════════════════════════════════════════════════════╝") println("╚══════════════════════════════════════════════════════════════╝")
@ -1014,7 +1071,6 @@ private suspend fun generatePdfToBytes(
} }
} }
} }
println("\n" + "".repeat(64))
}*/ }*/
// println("\n[FIN SCAN ROW $rowIdx]") // println("\n[FIN SCAN ROW $rowIdx]")
@ -1034,10 +1090,10 @@ private suspend fun generatePdfToBytes(
drawHairPinForTUO( drawHairPinForTUO(
cs = cs, cs = cs,
tuo = tuo, tuo = tuo,
gridColumnCount = colCount, // tu as déjà colCount gridColumnCount = colCount,
colX = x, colX = x,
colWidth = colWidth, colWidth = colWidth,
markerY = y, // ton y actuel markerY = y,
noteLineH = noteLineH noteLineH = noteLineH
) )
} }
@ -1093,6 +1149,27 @@ private suspend fun generatePdfToBytes(
rowTuos.forEachIndexed { colIdx, tuo -> rowTuos.forEachIndexed { colIdx, tuo ->
val x = marginX + colIdx * colWidth val x = marginX + colIdx * colWidth
// on dessine à droite
if (hasTrailingBarlineRow && isLastVisualRow && colIdx == rowTuos.size - 1) {
val finalX = x + colWidth
val sepToDraw = allRows.last().first().sep0
cs.setStrokingColor(0f, 0f, 0f)
cs.setLineWidth(0.8f)
val sepW = textWidth(customFont, noteFontSize, "|")
cs.moveTo(finalX - sepW, lineTop)
cs.lineTo(finalX - sepW, lineBottom)
cs.stroke()
if (sepToDraw == "/") {
cs.moveTo(finalX - (sepW * 0.5f), lineTop)
cs.lineTo(finalX - (sepW * 0.5f), lineBottom)
cs.stroke()
}
}
val sepW = textWidth(customFont, noteFontSize, "|") val sepW = textWidth(customFont, noteFontSize, "|")
val noteX = when (tuo.sep0) { val noteX = when (tuo.sep0) {
"/", "|" -> { "/", "|" -> {
@ -1110,8 +1187,7 @@ private suspend fun generatePdfToBytes(
val noteLines = tuo.noteAsMultiString().split("\n") val noteLines = tuo.noteAsMultiString().split("\n")
// println("\n=== ROW0 col[$colIdx] sep0='${tuo.sep0}' note=${noteLines[0]} ===")
// println("\n=== ROW0 col[$colIdx] sep0='${tuo.sep0}' noteX=$noteX ===")
// println(" noteAsMultiString raw = ${tuo.noteAsMultiString().replace("\n", "\\n")}") // println(" noteAsMultiString raw = ${tuo.noteAsMultiString().replace("\n", "\\n")}")
// println(" noteLines.size = ${noteLines.size}") // println(" noteLines.size = ${noteLines.size}")
noteLines.forEachIndexed { li, line -> noteLines.forEachIndexed { li, line ->
@ -1201,6 +1277,15 @@ private suspend fun generatePdfToBytes(
// println("║ lyricLineH = $lyricLineH") // println("║ lyricLineH = $lyricLineH")
// Numéro de stanza // Numéro de stanza
var hasLyrics = false
allTemps.forEachIndexed { sIdx, syllables ->
syllables.forEachIndexed { lineIdx, syl ->
if(syl.isNotEmpty()) hasLyrics=true
}
}
// println("hasL? = $hasLyrics")
if(hasLyrics) {
val stanzaNumTxt = "$currentStanza." val stanzaNumTxt = "$currentStanza."
val stanzaNumSize = lyricFontSize * 0.85f val stanzaNumSize = lyricFontSize * 0.85f
val stanzaNumFont = if (lyricFont is PDType0Font) lyricFont else customFont val stanzaNumFont = if (lyricFont is PDType0Font) lyricFont else customFont
@ -1215,25 +1300,6 @@ private suspend fun generatePdfToBytes(
cs.showText(stanzaNumTxt) cs.showText(stanzaNumTxt)
cs.endText() cs.endText()
// Syllabes
/*allTemps.forEachIndexed { sIdx, syllables ->
val x = marginX + sIdx * colWidth
val noteX = if (rowTuos.getOrNull(sIdx)?.sep0 in listOf("/", "|")) x + 3f else x
syllables.forEachIndexed { lineIdx, syl ->
val (spacedSyl, alignLeft) = spacedSyllable(
syl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize
)
val lyricX = if (alignLeft) noteX
else noteX + colWidth - textWidth(customFont, lyricFontSize, spacedSyl)
cs.beginText()
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
cs.newLineAtOffset(lyricX, yLyric - lineIdx * lyricLineH)
cs.showText(spacedSyl.sanitize())
cs.endText()
}
}*/
fun drawInOverrideSlot( fun drawInOverrideSlot(
cs: PDPageContentStream, cs: PDPageContentStream,
targetIdx: Int, // 0=S, 1=A, 2=T, 3=B, ou -1 pour le remplacement direct de syl0 targetIdx: Int, // 0=S, 1=A, 2=T, 3=B, ou -1 pour le remplacement direct de syl0
@ -1320,21 +1386,25 @@ private suspend fun generatePdfToBytes(
val cleanSyl0 = cleanSyllable(rawSyl0) val cleanSyl0 = cleanSyllable(rawSyl0)
val cleanSyl = cleanSyllable(rawSyl) val cleanSyl = cleanSyllable(rawSyl)
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) || val syl0LyricsIsFree = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
cleanSyl0.all { it == '―' || it == '-' || it == '—' } cleanSyl0.all { it == '―' || it == '-' || it == '—' }
val isAllZeroSpacing = customSpacings.all { it == 0 } val isAllZeroSpacing = customSpacings.all { it == 0 }
if (prefixes.contains(1) && isAllZeroSpacing && lignePrincipaleEstLibre) { if (prefixes.contains(1) && isAllZeroSpacing && syl0LyricsIsFree) {
// FORCE LE DESSIN SUR LA LIGNE PRINCIPALE (SYL0) println("DEBUG: Force SYL1 sur ligne principale (Rouge) -> $cleanSyl")
val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize) val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/ val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/
cs.saveGraphicsState()
cs.beginText() cs.beginText()
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize) cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
cs.setNonStrokingColor(Color.RED)
cs.newLineAtOffset(lyricX, currentYLyric) cs.newLineAtOffset(lyricX, currentYLyric)
cs.showText(spacedSyl.sanitize()) cs.showText(spacedSyl.sanitize())
cs.endText() cs.endText()
cs.restoreGraphicsState()
} else if (isAllZeroSpacing) { } else if (isAllZeroSpacing) {
// ── CAS SIMPLE : pas d'espace entre notes → syl1+ descend sous syl0 ── // ── CAS SIMPLE : pas d'espace entre notes → syl1+ descend sous syl0 ──
@ -1359,8 +1429,6 @@ private suspend fun generatePdfToBytes(
val rawSyl0 = syllables.getOrNull(0) ?: "" val rawSyl0 = syllables.getOrNull(0) ?: ""
val cleanSyl0 = cleanSyllable(rawSyl0) val cleanSyl0 = cleanSyllable(rawSyl0)
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
val hasActualText = REGEX_HAS_LYRIC.containsMatchIn(cleanSyl) && val hasActualText = REGEX_HAS_LYRIC.containsMatchIn(cleanSyl) &&
@ -1374,17 +1442,7 @@ private suspend fun generatePdfToBytes(
// --- CAS SPÉCIAL : LE PRÉFIXE "1." (SOPRANO) --- // --- CAS SPÉCIAL : LE PRÉFIXE "1." (SOPRANO) ---
if (voiceNum == 1) { if (voiceNum == 1) {
// A. PRIORITÉ : Si la ligne principale est libre, on s'installe dessus if (customSpacings.getOrElse(0) { 0 } == 1 && !filledSlots.contains(0)) {
if (lignePrincipaleEstLibre && !filledSlots.contains(-1)) {
drawInOverrideSlot(
cs, -1, cleanSyl, sIdx, lineIdx, allTemps, noteX, colWidth,
customFont, lyricFontSize, currentYLyric, noteLineH, customSpacings,
isDirectY = true // Pose sur yLyric
)
filledSlots.add(-1)
}
// B. REPLI : Si la ligne 0 est occupée, on regarde si un espace (1) est ouvert dans cS
else if (customSpacings.getOrElse(0) { 0 } == 1 && !filledSlots.contains(0)) {
drawInOverrideSlot( drawInOverrideSlot(
cs, cs,
0, 0,
@ -1443,6 +1501,7 @@ private suspend fun generatePdfToBytes(
if (bottomOfCurrentStanza < lowestYOfThisRow) { if (bottomOfCurrentStanza < lowestYOfThisRow) {
lowestYOfThisRow = bottomOfCurrentStanza lowestYOfThisRow = bottomOfCurrentStanza
} }
}
// val bottomOfStanza = yLyric - (maxLyricLines * lyricLineH) // val bottomOfStanza = yLyric - (maxLyricLines * lyricLineH)
// if (bottomOfStanza < lowestYOfThisRow) lowestYOfThisRow = bottomOfStanza // if (bottomOfStanza < lowestYOfThisRow) lowestYOfThisRow = bottomOfStanza
} }
@ -1494,7 +1553,7 @@ private suspend fun generatePdfToBytes(
// println(" DC[$tuoIdx]: '$dcPart'") // println(" DC[$tuoIdx]: '$dcPart'")
} }
if (tuo.pTemplate.markerToString().contains("DC")) { if (tuo.pTemplate.markerToString().contains("DC")) {
// println(" 🎯 DC MARQUEUR[$tuoIdx]") // println(" DC MARQUEUR[$tuoIdx]")
} }
} }
@ -1509,15 +1568,14 @@ private suspend fun generatePdfToBytes(
val dcPart = syls.drop(1).joinToString(" ") { it.trim() } val dcPart = syls.drop(1).joinToString(" ") { it.trim() }
// ✅ 2e : NORMAL (si existe) // NORMAL (si existe)
if (normal.isNotEmpty()) { if (normal.isNotEmpty()) {
result.append(normal).append(" ") result.append(normal).append(" ")
// println(" → AJOUT NORMAL[$tuoIdx]: '$normal'")
} }
// ✅ 1er : CROCHET DC (TOUJOURS, même si normal vide) // CROCHET DC (TOUJOURS, même si normal vide)
if (dcPart.isNotEmpty()) { if (dcPart.isNotEmpty()) {
result.append("[$dcPart] ") // dcPart
// println(" 🟦 CROCHET DC[$tuoIdx]: '[$dcPart]'") result.append("[${cleanSyllable(dcPart)}] ")
} }
// 3. DC MARQUEUR (parenthèses) // 3. DC MARQUEUR (parenthèses)
@ -1739,7 +1797,7 @@ private suspend fun generatePdfToBytes(
os.contains("win") -> Runtime.getRuntime().exec(arrayOf("explorer", outputDir)) os.contains("win") -> Runtime.getRuntime().exec(arrayOf("explorer", outputDir))
} }
emmentalerTTF.close() emmentalerTTF.close()
println(" PDF : $outputPath")*/ println(" PDF : $outputPath")*/
val computedFileName = "${cleanTitle}_R_${rStr}_U_${uStr}.pdf" val computedFileName = "${cleanTitle}_R_${rStr}_U_${uStr}.pdf"
// Sérialiser en mémoire // Sérialiser en mémoire

View file

@ -0,0 +1,51 @@
package mg.dot.feufaro.pdf
import org.apache.pdfbox.pdmodel.common.PDRectangle
actual data class PrintSettings actual constructor(
actual val pageSize : PageSize,
actual val orientation : PageOrientation,
actual val markerHeight : Float,
actual val noteFontSize : Float,
actual val lyricFontSize : Float,
actual val noteLineH : Float,
actual val lyricLineH : Float,
actual val extraSylLineH : Float,
actual val adjustedMargin : Float,
actual val interRowGap : Float,
actual val marginX : Float,
actual val marginY : Float,
actual val headerH : Float,
actual val nbGridOnMeasure: Int
) {
val pdRectangle: PDRectangle
get() {
val base = when (pageSize) {
PageSize.A4 -> PDRectangle.A4
PageSize.A3 -> PDRectangle.A3
PageSize.LETTER -> PDRectangle.LETTER
}
return if (orientation == PageOrientation.PORTRAIT) base
else PDRectangle(base.height, base.width)
}
actual val pageWidth : Float get() = pdRectangle.width
actual val pageHeight : Float get() = pdRectangle.height
}
actual fun defaultPrintSettings(): PrintSettings = PrintSettings(
pageSize = PageSize.A4,
orientation = PageOrientation.PORTRAIT,
markerHeight = 16f,
noteFontSize = 12f,
lyricFontSize = 12.5f,
noteLineH = 17f,
lyricLineH = 15f,
extraSylLineH = 15f,
adjustedMargin = 14f,
interRowGap = 22f,
marginX = 42.5f,
marginY = 42.5f,
headerH = 55f,
nbGridOnMeasure = 0
)