All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tri.util.ui.pdf.PdfViewer.kt Maven / Gradle / Ivy

The newest version!
/*-
 * #%L
 * tri.promptfx:promptfx
 * %%
 * Copyright (C) 2023 - 2025 Johns Hopkins University Applied Physics Laboratory
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package tri.util.ui.pdf

import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import javafx.beans.binding.Bindings
import javafx.beans.binding.BooleanBinding
import javafx.beans.property.*
import javafx.embed.swing.SwingFXUtils
import javafx.scene.control.ScrollPane
import javafx.scene.effect.BlurType
import javafx.scene.effect.DropShadow
import javafx.scene.image.Image
import javafx.scene.image.WritableImage
import javafx.scene.layout.Priority
import javafx.scene.paint.Color
import javafx.stage.Screen
import okhttp3.internal.closeQuietly
import org.apache.pdfbox.Loader
import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.pdmodel.PDPage
import org.apache.pdfbox.rendering.PDFRenderer
import org.apache.poi.util.IOUtils
import tornadofx.*
import java.io.FileInputStream
import java.io.InputStream
import java.net.URI
import java.net.URLDecoder

/**
 * This code is adapted from https://github.com/edvin/tornadofx-samples/blob/master/pdf-viewer/src/main/kotlin/pdf/Main.kt.
 */
class PdfViewer : Fragment("") {

    val viewModel = PdfViewModel()

    init {
        titleProperty.bind(viewModel.documentURIString)
    }

    override val root = borderpane {
        hgrow = Priority.ALWAYS
        vgrow = Priority.ALWAYS

        top = toolbar {
            spacer()
            button("", FontAwesomeIconView(FontAwesomeIcon.ANGLE_DOUBLE_LEFT)) {
                tooltip("Go to first page")
                action { viewModel.currentPageNumber.value = 0 }
                disableWhen(viewModel.isFirst)
            }
            button("", FontAwesomeIconView(FontAwesomeIcon.ANGLE_LEFT)) {
                tooltip("Go to previous page")
                action { viewModel.currentPageNumber.value-- }
                disableWhen(viewModel.isFirst)
            }
            textfield("") {
                viewModel.currentPageNumber.onChange { text = (it + 1).toString() }
                action { viewModel.currentPageNumber.value = text.toInt() - 1 }
                prefColumnCount = 3
            }
            label(Bindings.createStringBinding({ "/ " + viewModel.pageCount.value }, viewModel.pageCount)) {
                style { fontSize = 12.px }
            }
            button("", FontAwesomeIconView(FontAwesomeIcon.ANGLE_RIGHT)) {
                tooltip("Go to next page")
                action { viewModel.currentPageNumber.value++ }
                disableWhen(viewModel.isLast)
            }
            button("", FontAwesomeIconView(FontAwesomeIcon.ANGLE_DOUBLE_RIGHT)) {
                tooltip("Go to last page")
                action { viewModel.currentPageNumber.value = viewModel.pageCount.value - 1 }
                disableWhen(viewModel.isLast)
            }
            spacer()
            button("", FontAwesomeIconView(FontAwesomeIcon.FILE_PDF_ALT)) {
                tooltip("Open in system")
                action { hostServices.showDocument(viewModel.documentURIString.value) }
            }
        }
        center {
            scrollpane {
                hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
                vbarPolicy = ScrollPane.ScrollBarPolicy.ALWAYS
                isFitToHeight = true
                style {
                    padding = box(0.px, 40.px, 0.px, 40.px)
                    backgroundColor += Color.DARKGRAY
                }
                stackpane {
                    imageview(viewModel.currentPage) {
                        style {
                            borderColor += box(Color.BLACK)
                            effect = DropShadow(BlurType.THREE_PASS_BOX, Color.GRAY, 10.0, 0.0, 0.0, 0.0)
                        }
                        fitWidthProperty().bind([email protected]() - 80)
                        isPreserveRatio = true
                        isSmooth = true
                        isCache = true
                    }
                }
            }
        }

        // set pref height to 75% of the height of the monitor on which the current stage is showing
        val curScreen = Screen.getScreensForRectangle(primaryStage.x, primaryStage.y, 1.0, 1.0).firstOrNull()
            ?: Screen.getPrimary()
        prefHeight = curScreen.bounds.height * 0.75
    }
}

/** Model for [PdfViewer]. */
class PdfViewModel : ViewModel() {
    var documentURIString = SimpleStringProperty("")
    private var documentURI = SimpleObjectProperty(URI(""))
    private var documentInputStream = SimpleObjectProperty()
    private var document: PDDocument? = null
    private var pdfRenderer: PDFRenderer? = null
    val currentPage = SimpleObjectProperty(WritableImage(1, 1))
    val currentPageNumber = SimpleIntegerProperty(0)
    val pageCount = SimpleIntegerProperty(0)
    private val scale = SimpleFloatProperty(1.25f)

    var isFirst: BooleanBinding = currentPageNumber.isEqualTo(0)
    var isLast: BooleanBinding = currentPageNumber.isEqualTo(pageCount - 1)

    init {
        documentInputStream.onChange { input ->
            document?.closeQuietly()
            if (input is InputStream) {
                val bytes = IOUtils.toByteArray(input)
                document = Loader.loadPDF(bytes)
                pdfRenderer = PDFRenderer(document)
                pageCount.value = document?.pages?.count
                document?.getPage(0)?.let { setScaleFor(it) }
                openPage(0)
            }
        }
        documentURIString.onChange { documentURI.value = URI(it) }
        documentURI.onChange {
            val input = when (it!!.scheme) {
                "file" -> FileInputStream(URLDecoder.decode(it.path, "UTF-8"))
                else -> null
            }
            documentInputStream.value = input
        }
        currentPageNumber.addListener { _, old, nue ->
            try {
                openPage(nue.toInt())
            } catch (x: IndexOutOfBoundsException) {
                currentPageNumber.set(old.toInt())
            }
        }
    }

    private fun setScaleFor(page: PDPage) {
        val box = page.mediaBox
        if (box != null) {
            val width = box.width
            val height = box.height
            val screen = Screen.getPrimary()
            val screenWidth = screen.bounds.width.toFloat() * .75f
            val screenHeight = screen.bounds.height.toFloat() * .75f
            val scaleWidth = screenWidth / width
            val scaleHeight = screenHeight / height
            scale.value = if (scaleWidth < scaleHeight) scaleWidth else scaleHeight
        }
    }

    private fun openPage(pageCounter: Int) {
        currentPageNumber.value = pageCounter
        val bim = pdfRenderer?.renderImage(pageCounter, scale.value)
        if (bim != null) {
            currentPage.value = SwingFXUtils.toFXImage(bim, null)
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy