Skip to content

Commit

Permalink
Fix iText7 engine and height computation errors
Browse files Browse the repository at this point in the history
  • Loading branch information
gregorbg committed Jul 29, 2022
1 parent 4e41555 commit 986cc7e
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ class GeneralScrambleSheet(
baseUnit: Float,
labelPrefix: String? = null
) {
val useHighlighting = scramblePhrases.any { it.lineTokens.size >= MIN_LINES_HIGHLIGHTING }
val highestLineCount = scramblePhrases.maxOf { it.lineTokens.size }
val useHighlighting = highestLineCount >= MIN_LINES_HIGHLIGHTING

leading = 1f

for ((index, scramble) in scramblePhrases.withIndex()) {
row {
Expand All @@ -50,7 +53,8 @@ class GeneralScrambleSheet(
horizontalAlignment = Alignment.Horizontal.JUSTIFIED
verticalAlignment = Alignment.Vertical.MIDDLE

padding = 2 * Drawing.Padding.DEFAULT
val paddingFactor = if (highestLineCount == 1) 1 else 2
padding = paddingFactor * 2 * Drawing.Padding.DEFAULT

paragraph {
fontName = Font.MONO
Expand Down Expand Up @@ -170,8 +174,8 @@ class GeneralScrambleSheet(

val scramblePhrases = computeScramblePhrases(
scramblePageChunk,
relHeightPerScramble * 0.85f,
scrambleTextWidth * 0.975f,
relHeightPerScramble * 0.75f,
scrambleTextWidth * 0.95f,
actualWidthIn
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import java.time.LocalDate
import kotlin.math.atan

object IText7Engine {
const val CREATOR_STRING = "iText 7" // TODO is there a cleaner alternative?

fun render(doc: Document, password: String? = null): ByteArray {
val baos = ByteArrayOutputStream()
val creationDate = LocalDate.now() // TODO pass in from the outside world?
Expand All @@ -48,20 +50,20 @@ object IText7Engine {
val writer = PdfWriter(baos, properties)

val pdfDocument = PdfDocument(writer)
val modelDocument = com.itextpdf.layout.Document(pdfDocument)
val itextDocument = com.itextpdf.layout.Document(pdfDocument)

val fontIndex = doc.exposeFontNames()
.associateWith { PdfFontFactory.createFont("fonts/$it.ttf", PdfEncodings.IDENTITY_H, pdfDocument) }

pdfDocument.documentInfo.addCreationDate()
pdfDocument.documentInfo.producer = "iText 7" // TODO is there a cleaner alternative?
pdfDocument.documentInfo.producer = CREATOR_STRING
pdfDocument.documentInfo.title = doc.title

for ((n, page) in doc.pages.withIndex()) {
val itextPageSize = convertPageSize(page.size)
pdfDocument.defaultPageSize = itextPageSize

modelDocument.setMargins(
itextDocument.setMargins(
page.marginTop.toFloat(),
page.marginRight.toFloat(),
page.marginBottom.toFloat(),
Expand All @@ -75,16 +77,6 @@ object IText7Engine {
addedPage.addWatermark(doc.watermark, monoFont, n + 1)
}

for (element in page.elements) {
val itextElement = render(element, pdfDocument, fontIndex)

if (itextElement is IBlockElement) {
modelDocument.add(itextElement)
} else if (itextElement is Image) {
modelDocument.add(itextElement)
}
}

val headerHeight = 3 * page.marginTop.toFloat() / 4
val footerHeight = 3 * page.marginBottom.toFloat() / 4

Expand All @@ -99,9 +91,19 @@ object IText7Engine {

if (page.footerLine != null)
addedPage.showFooterCenter(page.footerLine, itextPageSize, footerHeight)

for (element in page.elements) {
val itextElement = render(element, pdfDocument, fontIndex)

if (itextElement is IBlockElement) {
itextDocument.add(itextElement)
} else if (itextElement is Image) {
itextDocument.add(itextElement)
}
}
}

modelDocument.close()
itextDocument.close()

return baos.toByteArray()
}
Expand All @@ -124,7 +126,7 @@ object IText7Engine {
}

private fun PdfPage.writeTextOutOfBounds(text: String, horizontalPos: Float, verticalPos: Float) {
val over = PdfCanvas(this)
val over = PdfCanvas(newContentStreamBefore(), resources, document)

Canvas(over, pageSize)
.showTextAligned(
Expand Down Expand Up @@ -267,9 +269,13 @@ object IText7Engine {
itextCell.setBorderAround(borderType, cell.border)
itextCell.setPadding(cell.padding.toFloat())
itextCell.setTextAlignment(convertTextAlignment(cell.horizontalAlignment))
//itextCell.setPaddingTop(-0.5f) // TODO HACK

val renderedContent = render(cell.content.innerElement, pdfDocument, fontIndex)
val innerElement = cell.content.innerElement

val renderedContent = if (innerElement is Text) {
val mockParagraph = Paragraph(1f, listOf(innerElement))
render(mockParagraph, pdfDocument, fontIndex)
} else render(cell.content.innerElement, pdfDocument, fontIndex)

if (renderedContent is IBlockElement) {
itextCell.add(renderedContent)
Expand All @@ -278,12 +284,6 @@ object IText7Engine {
renderedContent.setHorizontalAlignment(convertAlignment(cell.horizontalAlignment))

itextCell.add(renderedContent)
} else if (renderedContent is com.itextpdf.layout.element.Text) {
val nestedParagraph = com.itextpdf.layout.element.Paragraph(renderedContent)
.setMultipliedLeading(1f)
//.setPaddingTop(-2f) // TODO hack!

itextCell.add(nestedParagraph)
}

return itextCell
Expand All @@ -294,7 +294,7 @@ object IText7Engine {
fontIndex: Map<String, PdfFont>
): com.itextpdf.layout.element.Paragraph {
val itextParagraph = com.itextpdf.layout.element.Paragraph()
itextParagraph.setMultipliedLeading(paragraph.leading * 0.6f) // TODO HACK
itextParagraph.setMultipliedLeading(paragraph.leading)

for (line in paragraph.lines) {
val renderedLine = renderText(line, fontIndex)
Expand Down Expand Up @@ -344,6 +344,9 @@ object IText7Engine {
val writer = PdfWriter(baos, properties)
val pdfDocument = PdfDocument(writer)

pdfDocument.documentInfo.addCreationDate()
pdfDocument.documentInfo.producer = CREATOR_STRING

val root = pdfDocument.getOutlines(false)

val outlineGroupCache = mutableMapOf<String, PdfOutline>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ import com.itextpdf.kernel.font.PdfFont
import com.itextpdf.kernel.font.PdfFontFactory
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.model.properties.Font
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.model.properties.Paper
import org.worldcubeassociation.tnoodle.server.webscrambles.zip.util.StringUtil.stripNewlines
import kotlin.math.abs
import kotlin.math.min

object FontUtil {
private val FONT_PROGRAM_CACHE = mutableMapOf<String, PdfFont>()

private const val FLOAT_EPSILON = 0.0001f

fun Float.epsilonEqual(f: Float): Boolean {
return abs(this - f) < FLOAT_EPSILON
}

fun computeFontScale(text: String, fontName: String, bbRelativeWidth: Float): Float {
val font = FONT_PROGRAM_CACHE.getOrPut(fontName) {
PdfFontFactory.createFont("fonts/$fontName.ttf", PdfEncodings.IDENTITY_H)
Expand All @@ -33,18 +41,29 @@ object FontUtil {
return optimalFontScale * unitToInches * height * Paper.DPI
}

fun generatePhrase(scramble: String, isExtra: Boolean, boxHeight: Float, boxWidth: Float, baseUnit: Float): ScramblePhrase {
val chunksWithBreakFlags = ScramblePhrase.splitToChunks(scramble)

val oneLineScramble = chunksWithBreakFlags.joinToString(" ", prefix = ScramblePhrase.NBSP_STRING) { it.first }
val oneLineFontSize = computeOptimalOneLineFontSize(oneLineScramble, boxHeight, boxWidth, Font.MONO, baseUnit)
fun generatePhrase(
scramble: String,
isExtra: Boolean,
boxHeight: Float,
boxWidth: Float,
baseUnit: Float
): ScramblePhrase {
// at first, try one-line (silly for big events but not computation-intensive at all)
val oneLineScramble = ScramblePhrase.NBSP_STRING + scramble + ScramblePhrase.NBSP_STRING
// TODO magic number same as genericSheet phrase computation
val oneLineFontSize = computeOptimalOneLineFontSize(oneLineScramble, boxHeight, boxWidth * 0.95f, Font.MONO, baseUnit)

// can we fit the entire scramble on one line without making it terribly small
if (oneLineFontSize > ScramblePhrase.MIN_ONE_LINE_FONT_SIZE) {
// we can fit the entire scramble on one line without making it terribly small
val oneLineTokens = chunksWithBreakFlags.map { it.first }
return ScramblePhrase(scramble, isExtra, chunksWithBreakFlags, listOf(oneLineTokens), oneLineFontSize)
// if the scramble ends up on one line, there's no need for padding individual moves
val oneLineRawTokens = scramble.stripNewlines().split(" ")
val oneLineChunksWithFlags = oneLineRawTokens.map { it to false }

return ScramblePhrase(scramble, isExtra, oneLineChunksWithFlags, listOf(oneLineRawTokens), oneLineFontSize)
}

val chunksWithBreakFlags = ScramblePhrase.splitToChunks(scramble)

val phraseChunks = splitAtPossibleBreaks(chunksWithBreakFlags)
val lineTokens = splitToLines(phraseChunks, boxHeight, boxWidth)

Expand Down Expand Up @@ -111,7 +130,10 @@ object FontUtil {

val (candidateLine, rest) = takeChunksThatFitOneLine(chunksSections, boxRelativeWidth, ScramblePhrase.NBSP_STRING)

val currentLine = if (candidateLine.isEmpty()) chunksSections.first() else candidateLine
val currentLine = candidateLine.ifEmpty {
chunksSections.first().toMutableList().apply { add(0, ScramblePhrase.NBSP_STRING) }
}

val actRest = if (candidateLine.isEmpty()) chunksSections.drop(1) else rest

return splitCurrentToLines(actRest, boxRelativeWidth, merge(accu, currentLine))
Expand All @@ -130,7 +152,11 @@ object FontUtil {
val nextAccu = currentAccu + chunkSections.first()
val joinedChunk = nextAccu.joinToString(" ")

if (computeFontScale(joinedChunk, Font.MONO, boxRelativeWidth) < 1f) {
val nextChunkFontScale = computeFontScale(joinedChunk, Font.MONO, boxRelativeWidth)

// fontScale is by definition never more than 1 so checking for "not equal" suffices.
if (!nextChunkFontScale.epsilonEqual(1f)) {
// the next chunk does NOT fit on the line anymore.
if (lineEndPadding != null) {
val paddedAccu = currentAccu + lineEndPadding
val paddedChunk = paddedAccu.joinToString(" ")
Expand All @@ -142,7 +168,11 @@ object FontUtil {
return emptyList<String>() to chunkSections
}

if (computeFontScale(paddedChunk, Font.MONO, boxRelativeWidth) == 1f) {
val paddedFontScale = computeFontScale(paddedChunk, Font.MONO, boxRelativeWidth)

if (paddedFontScale.epsilonEqual(1f)) {
// Top version: pad at most once. Bottom version: pad as much as possible.
// return paddedAccu to chunkSections
return takeChunksThatFitOneLine(chunkSections, boxRelativeWidth, lineEndPadding, paddedAccu)
}
}
Expand All @@ -160,6 +190,11 @@ object FontUtil {
accu: List<List<String>> = emptyList()
): List<List<String>> {
if (chunksWithBreakFlags.isEmpty()) {
if (accu.isEmpty()) {
// not a single line break in the entire sequence.
return listOf(currentPhraseAccu)
}

return accu
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ data class ScramblePhrase(
companion object {
val NBSP_STRING = Typography.nbsp.toString()

val MIN_ONE_LINE_FONT_SIZE = 15f
val MAX_PHRASE_FONT_SIZE = 20f
val MIN_ONE_LINE_FONT_SIZE = 12.5f
val MAX_PHRASE_FONT_SIZE = 22f

fun padTurnsUniformly(scramble: String, padding: String = NBSP_STRING): String {
val maxTurnLength = scramble.split("\\s+".toRegex()).maxOfOrNull { it.length } ?: 0
Expand Down

0 comments on commit 986cc7e

Please sign in to comment.