Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve PDF font size detection #652

Merged
merged 3 commits into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import com.itextpdf.text.pdf.PdfContentByte
import com.itextpdf.text.pdf.PdfPCell
import com.itextpdf.text.pdf.PdfPTable
import com.itextpdf.text.pdf.PdfWriter
import org.worldcubeassociation.tnoodle.server.model.EventData
import org.worldcubeassociation.tnoodle.svglite.Dimension
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.FontUtil
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.PdfDrawUtil.renderSvgToPDF
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.PdfUtil
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.PdfUtil.splitToLineChunks
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.StringUtil
import org.worldcubeassociation.tnoodle.server.webscrambles.wcif.model.*
import kotlin.math.min

Expand All @@ -36,7 +38,7 @@ class GeneralScrambleSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode)

val scrambleImageSize = scramblingPuzzle.getPreferredSize(maxScrambleImageWidth, maxScrambleImageHeight)

val allScrambleStrings = scrambleSet.allScrambles.toPDFStrings(scramblingPuzzle.shortName)
val allScrambleStrings = scrambleSet.allScrambles.toPDFStrings(activityCode.eventModel)

val scrambleImageHeight = scrambleImageSize.height.toFloat()
val scrambleColumnWidth = availableWidth - indexColumnWidth - scrambleImageSize.width
Expand All @@ -59,7 +61,11 @@ class GeneralScrambleSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode)
isLockedWidth = true
}

val extraScramblesHeader = PdfPCell(Paragraph(TABLE_HEADING_EXTRA_SCRAMBLES)).apply {
val extraParagraph = Paragraph(TABLE_HEADING_EXTRA_SCRAMBLES).apply {
font.style = Font.BOLD
}

val extraScramblesHeader = PdfPCell(extraParagraph).apply {
verticalAlignment = PdfPCell.ALIGN_MIDDLE
horizontalAlignment = PdfPCell.ALIGN_CENTER
fixedHeight = extraScrambleLabelHeight
Expand All @@ -75,20 +81,41 @@ class GeneralScrambleSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode)
}

private fun getFontConfiguration(availableArea: Rectangle, scrambles: List<String>): Font {
val longestScramble = scrambles.flatMap { it.split(NEW_LINE) }.maxByOrNull { it.length }.orEmpty()
val maxLines = scrambles.map { it.split(NEW_LINE) }.map { it.count() }.maxOrNull() ?: 1
val paddedScrambles = scrambles.map { StringUtil.padTurnsUniformly(it, Typography.nbsp.toString()) }

val longestScrambleLine = paddedScrambles.flatMap { it.split(NEW_LINE) }.maxByOrNull { it.length }.orEmpty()
val maxLinesCount = paddedScrambles.map { it.split(NEW_LINE) }.maxOfOrNull { it.count() } ?: 1

val fontSize = PdfUtil.fitText(Font(FontUtil.MONO_FONT), longestScramble, availableArea, FontUtil.MAX_SCRAMBLE_FONT_SIZE, true) // FIXME const
val fontSizeIfIncludingNewlines = availableArea.height / maxLines
val scramblesHaveLineBreaks = scrambles.any { NEW_LINE in it }

val fontSize = fitFontSize(availableArea, longestScrambleLine, scramblesHaveLineBreaks)
val maxFontSizePerLine = availableArea.height / maxLinesCount

// fontSize should fit horizontally. fontSizeIfIncludingNewlines should fit considering \n
// In case maxLines = 1, fontSizeIfIncludingNewlines is just ignored (as 1 font size should fill the whole rectangle's height)
// in case we have maxLines > 1, we fit width or height and take the min of it.
val perfectFontSize = min(fontSize, fontSizeIfIncludingNewlines)
val perfectFontSize = min(fontSize, maxFontSizePerLine)

return Font(FontUtil.MONO_FONT, perfectFontSize, Font.NORMAL)
}

private fun fitFontSize(availableArea: Rectangle, longestScrambleLine: String, scramblesHaveLineBreaks: Boolean = false): Float {
val fittingFont = Font(FontUtil.MONO_FONT)

return if (scramblesHaveLineBreaks) {
// if our scrambles dictate line breaks, then just calculate the fitting text size right away
PdfUtil.fitText(fittingFont, longestScrambleLine, availableArea, FontUtil.MAX_SCRAMBLE_FONT_SIZE, true)
} else {
// try to fit them on one line
val oneLineFontSize = PdfUtil.fitText(fittingFont, longestScrambleLine, availableArea, FontUtil.MAX_SCRAMBLE_FONT_SIZE, false)

// is the optimal font size too small to read?
if (oneLineFontSize < FontUtil.MINIMUM_ONE_LINE_FONT_SIZE) {
PdfUtil.fitText(fittingFont, longestScrambleLine, availableArea, FontUtil.MAX_SCRAMBLE_FONT_SIZE, true)
} else oneLineFontSize
}
}

private fun requiresHighlighting(scrambleColumnWidth: Float, scrambleFont: Font, scrambles: List<String>): Boolean {
val lineChunks = scrambles.map { it.splitToLineChunks(scrambleFont, scrambleColumnWidth) }

Expand All @@ -102,7 +129,7 @@ class GeneralScrambleSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode)
isLockedWidth = true
}

val strScrambles = scrambles.toPDFStrings(scramblingPuzzle.shortName)
val strScrambles = scrambles.toPDFStrings(activityCode.eventModel)

for ((i, scramble) in strScrambles.withIndex()) {
val indexCell = PdfPCell(Paragraph("$scrambleNumberPrefix${i + 1}")).apply {
Expand All @@ -126,6 +153,9 @@ class GeneralScrambleSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode)
// let itextpdf wrap lines for us.
isNoWrap = true
verticalAlignment = PdfPCell.ALIGN_MIDDLE
// iText5 defines strange default margins, so we push the text up artificially
paddingTop = SCRAMBLE_CELL_TOP_MARGIN.toFloat()
setLeading(0f, SCRAMBLE_CELL_MULTIPLIED_LEADING.toFloat())
}

table.addCell(scrambleCell)
Expand All @@ -150,16 +180,18 @@ class GeneralScrambleSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode)
}

companion object {
private fun List<Scramble>.toPDFStrings(puzzleName: String) =
private fun List<Scramble>.toPDFStrings(event: EventData?) =
flatMap { it.allScrambleStrings }
.takeUnless { puzzleName == "minx" } // minx scrambles intentionally include "\n" chars for alignment
.takeUnless { event == EventData.MEGA } // Megaminx scrambles intentionally include "\n" chars for alignment
?: map { it.scrambleString }

const val MAX_SCRAMBLES_PER_PAGE = 7
const val MAX_SCRAMBLE_IMAGE_RATIO = 3
const val SCRAMBLE_IMAGE_PADDING = 2

const val SCRAMBLE_MARGIN = 5
const val SCRAMBLE_CELL_TOP_MARGIN = -2
const val SCRAMBLE_CELL_MULTIPLIED_LEADING = 1.07

const val EMPTY_CELL_CONTENT = ""

Expand All @@ -173,10 +205,10 @@ class GeneralScrambleSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode)
private val HIGHLIGHT_COLOR = BaseColor(230, 230, 230)

const val INDEX_COLUMN_WIDTH_RATIO = 25
const val EXTRA_SCRAMBLES_HEIGHT_RATIO = 30
const val EXTRA_SCRAMBLES_HEIGHT_RATIO = 35

const val VERTICAL_MARGIN = 15f
const val HORIZONTAL_MARGIN = 35f
const val VERTICAL_MARGIN = 15
const val HORIZONTAL_MARGIN = 35

const val NEW_LINE = "\n"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ object PdfUtil {

// Walk past all whitespace that comes immediately after
// the last line wrap we just inserted.
if (first() == ' ') {
if (first().isWhitespace()) {
return drop(1)
.splitLineToChunks(font, availableTextWidth, acc)
}
Expand Down