def getSearchResultHighlights()

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)
      }
    }
  }