in backend/app/utils/PDFUtil.scala [67:139]
def getSearchResultHighlights(highlights: List[TextHighlight], singlePageDoc: PDDocument, pageNumber: Int, isFind: Boolean = false): List[PageHighlight] = {
var textPositions = List.empty[Either[TextPosition, NewlinePlaceholder.type]]
val textStripper = new PDFTextStripper() {
// Looking up the stack, this is called for each line
override def writeString(notUsedText: String, javaTextPositions: util.List[TextPosition]): Unit = {
textPositions = textPositions ++ javaTextPositions.asScala.map(Left(_))
textPositions :+= Right(NewlinePlaceholder)
}
}
// Will call writeString above with every line
textStripper.getText(singlePageDoc)
highlights.zipWithIndex.map { case(highlight, ix) =>
val startIdx = highlight.range.startCharacter
val endIdx = highlight.range.endCharacter - 1
// Split the existing text path into lines of characters
// TODO SC: Make a case class for this to help readability?
val highlightSpans: List[List[TextPosition]] = textPositions
.slice(startIdx, endIdx + 1)
.zipWithIndex
.foldLeft(List(List[TextPosition]()))((acc, currWithIndex) => {
currWithIndex._1 match {
// Regular character just append to the last span
case Left(pos) => acc.init :+ (acc.last :+ pos)
// If there's a newline push a new span
case Right(NewlinePlaceholder) => acc :+ List()
}
})
val spans = highlightSpans.flatMap { span =>
for {
// Sometimes the text stripper doesn't pull out matches correctly resulting in empty spans
startCharacter <- span.headOption
endCharacter <- span.lastOption
} yield {
// This coordinate system makes my head hurt
val x = startCharacter.getX
val y = startCharacter.getY
val x1 = endCharacter.getX
val y1 = endCharacter.getY
val dX = x1 - x
val dY = y1 - y
val width = sqrt(dX * dX + dY * dY) + endCharacter.getWidth
val height = span.maxBy(_.getFontSize).getFontSize
val rotation = atan2(dY, dX)
// The highlight is rendered slightly off, we need to move the position by about 0.75 * the max character height
// while also factoring in rotation. Here were getting the x/y offsets by rotating a Y axis unit vector and then
// multiplying the result by the offset magnitude. The Y axis has to be negated due to the coordinate spaces.
val offsetMagnitude = height * 0.75
val offsetX = offsetMagnitude * sin(rotation)
val offsetY = -offsetMagnitude * cos(rotation)
HighlightSpan(x + offsetX, y + offsetY, width, height, rotation)
}
}
if (isFind) {
FindHighlight(highlight.id, highlight.index, spans)
} else {
SearchHighlight(highlight.id, highlight.index, spans)
}
}
}