* Added PDF top tab strip visibility toggle and fixed WebView hit test NPE * Refactored desktop reader screens and state management into specialized components * Added image gallery to reader sidebar and refactored desktop PDF UI components * Implemented EPUB image gallery * Refactored reader and library models to use shared common types, centralizing file type resolution and texture management while removing redundant mapping logic * Standardized UI styling and refactored app navigation layout * Implemented auto-hiding reader chrome and activity tracking in desktop * Refactored reader panels into distinct left and right modal layers with platform-specific sizing and keyboard navigation support. * Added PPTX support for desktop and refactored parsing into a shared module * Implement paid AI features and account management for the desktop application. * Implement AI Hub and enhanced Cloud TTS integration for Desktop * Implement streaming support for AI definition and summarization features * Implement support for password-protected PDFs and file actions in the desktop reader. * Implement cloud synchronization for desktop using Firestore and Google Drive * Implement PDF reflow and "Text View" for the desktop reader * Refactor OPDS logic to use SharedOpdsController * Optimize PDF tile rendering performance * Implement two-page spread support for PDF pagination * Implement two-page spread support for the PDF viewer * Improved shared spread zoom in PDF viewer * Improve PDF spread navigation with fling support and configurable page gaps * Add brightness control to PDF and EPUB readers * Refactor folder synchronization to use shared logic engine * Implement safe string formatting and validation for localized resources * Implement TTS chunk skip navigation * Implement deep-linking and playback controls for TTS media sessions * Implement start index for TTS playback * Improve TTS navigation, prefetching, and notification duration reporting * Implement TTS mini playback bar for background reading * Implement multi-window reader support for the desktop application * Improve desktop modal window management and visibility syncing * Implement localized string support for Desktop and shared UI * Implement language selection and persistence for Desktop * Implement plural string support for Desktop and migrate hardcoded counts to plurals.xml * Implement localized banner messages and UI strings using resource-backed SharedText * Implement compact badge styling for small book covers * Refactor PDF native interaction and improve HTML import memory safety * fix language persistence * Refactor reader overflow menus to use section-based logic * Refactor PDF layout remapping and improve text box interaction * Improve CFI resolution and TTS resume accuracy using dynamic chunk offsets * Centralize PDF annotation export mapping and improve metadata handling * Add support for threaded comments in PDF highlight annotations * Flatten highlight comments into a single thread for PDF export and allow author editing * Integrate page slider into reader chrome and persist toggle state * Handle fragments and queries in EPUB chapter paths * Implement dynamic, theme-aware coloring for the reader slider * Implement customizable app-wide font preference * Implement one-hand zoom gestures in the PDF viewer * Implement File Information dialog for PDF and EPUB readers * Bump version to 1.0.49 (53) * Refactor PDF reader logic into modular components * Add ProGuard rules to prevent R8 optimization issues in EPUB reader screens * Add option to use PDF filenames as display names * Fix preservation of PDF filename display preference in library projection
500 lines
18 KiB
Kotlin
500 lines
18 KiB
Kotlin
package com.aryan.reader.desktop
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.toArgb
|
|
import androidx.compose.ui.text.AnnotatedString
|
|
import androidx.compose.ui.text.SpanStyle
|
|
import androidx.compose.ui.text.font.FontStyle
|
|
import androidx.compose.ui.text.style.TextDecoration
|
|
import androidx.compose.ui.unit.isSpecified
|
|
import com.aryan.reader.shared.SaveMode
|
|
import com.aryan.reader.shared.pdf.PdfAnnotationKind
|
|
import com.aryan.reader.shared.pdf.SHARED_PDF_PAGE_BREAK_CHAR
|
|
import com.aryan.reader.shared.pdf.SharedPdfAnnotation
|
|
import com.aryan.reader.shared.pdf.SharedPdfAnnotationExportMapper
|
|
import com.aryan.reader.shared.pdf.SharedPdfRichPageLayout
|
|
import com.aryan.reader.shared.pdf.sharedPdfTextPageRelativeFontSize
|
|
import java.awt.FileDialog
|
|
import java.awt.Font
|
|
import java.awt.Frame
|
|
import java.awt.Graphics2D
|
|
import java.awt.RenderingHints
|
|
import java.awt.font.LineBreakMeasurer
|
|
import java.awt.font.TextAttribute
|
|
import java.awt.image.BufferedImage
|
|
import java.awt.print.Book
|
|
import java.awt.print.PageFormat
|
|
import java.awt.print.Printable
|
|
import java.awt.print.PrinterJob
|
|
import java.io.File
|
|
import java.text.AttributedString
|
|
import kotlin.math.ceil
|
|
import kotlin.math.roundToInt
|
|
import kotlin.random.Random
|
|
|
|
private const val DesktopPdfTextBoxPaddingPx = 8f
|
|
private const val DesktopPdfTextRasterPointScale = 3f
|
|
private const val DesktopPdfTextRasterMinPageHeightPx = 1200f
|
|
private const val DesktopPdfTextRasterMaxPageHeightPx = 3600f
|
|
private const val DesktopPdfRichTextMarginX = 0.1f
|
|
private const val DesktopPdfRichTextMarginY = 0.08f
|
|
|
|
internal data class DesktopPdfFileActionNotice(
|
|
val title: String,
|
|
val message: String,
|
|
val isError: Boolean = false
|
|
)
|
|
|
|
internal data class DesktopPdfRasterOverlay(
|
|
val pageIndex: Int,
|
|
val left: Float,
|
|
val top: Float,
|
|
val right: Float,
|
|
val bottom: Float,
|
|
val width: Int,
|
|
val height: Int,
|
|
val pixels: IntArray
|
|
)
|
|
|
|
internal fun desktopSuggestedPdfFilename(
|
|
originalName: String?,
|
|
isAnnotated: Boolean,
|
|
shortId: String = Random.nextInt(1000, 9999).toString()
|
|
): String {
|
|
val base = originalName
|
|
?.substringBeforeLast('.')
|
|
?.takeIf { it.isNotBlank() }
|
|
?: "Document"
|
|
val safeBase = base.replace("[^a-zA-Z0-9._-]".toRegex(), "_")
|
|
.take(50)
|
|
.ifBlank { "Document" }
|
|
val suffix = if (isAnnotated) "_annotated" else ""
|
|
return "${safeBase}${suffix}_$shortId.pdf"
|
|
}
|
|
|
|
internal fun hasExportableDesktopPdfAnnotations(
|
|
annotations: List<SharedPdfAnnotation>,
|
|
richTextPageLayouts: List<SharedPdfRichPageLayout>
|
|
): Boolean {
|
|
return SharedPdfAnnotationExportMapper.build(annotations).hasPdfAnnotations ||
|
|
annotations.any { annotation ->
|
|
if (annotation.kind != PdfAnnotationKind.HIGHLIGHT) return@any false
|
|
val startIndex = annotation.rangeStartIndex ?: return@any false
|
|
val endIndex = annotation.rangeEndIndex ?: return@any false
|
|
endIndex >= startIndex
|
|
} ||
|
|
annotations.any { annotation ->
|
|
annotation.kind == PdfAnnotationKind.TEXT &&
|
|
annotation.bounds != null &&
|
|
annotation.text.isNotBlank()
|
|
} ||
|
|
richTextPageLayouts.any { layout ->
|
|
layout.visibleText.text
|
|
.replace(SHARED_PDF_PAGE_BREAK_CHAR.toString(), "")
|
|
.isNotBlank()
|
|
}
|
|
}
|
|
|
|
internal fun shouldShowDesktopPdfAnnotationExportChoice(
|
|
sidecarsReady: Boolean,
|
|
annotations: List<SharedPdfAnnotation>,
|
|
richTextPageLayouts: List<SharedPdfRichPageLayout>
|
|
): Boolean {
|
|
return !sidecarsReady || hasExportableDesktopPdfAnnotations(annotations, richTextPageLayouts)
|
|
}
|
|
|
|
internal fun chooseSavePdfFile(defaultFileName: String): File? {
|
|
val dialog = FileDialog(null as Frame?, "Save PDF", FileDialog.SAVE).apply {
|
|
file = defaultFileName
|
|
isVisible = true
|
|
}
|
|
val directory = dialog.directory ?: return null
|
|
val fileName = dialog.file ?: return null
|
|
val selected = File(directory, fileName)
|
|
return if (selected.extension.equals("pdf", ignoreCase = true)) {
|
|
selected
|
|
} else {
|
|
File(selected.parentFile, "${selected.name}.pdf")
|
|
}
|
|
}
|
|
|
|
internal fun saveDesktopPdfCopy(
|
|
document: DesktopPdfDocument,
|
|
target: File,
|
|
mode: SaveMode,
|
|
annotations: List<SharedPdfAnnotation> = emptyList(),
|
|
richTextPageLayouts: List<SharedPdfRichPageLayout> = emptyList()
|
|
) {
|
|
val source = File(document.path)
|
|
require(source.isFile) { "The original PDF is not available as a local file." }
|
|
require(document.formatLabel == "PDF") { "Only PDF files can be saved as PDF copies." }
|
|
target.parentFile?.mkdirs()
|
|
|
|
when (mode) {
|
|
SaveMode.ORIGINAL -> {
|
|
if (source.canonicalFile == target.canonicalFile) return
|
|
source.copyTo(target, overwrite = true)
|
|
}
|
|
SaveMode.ANNOTATED -> {
|
|
require(source.canonicalFile != target.canonicalFile) {
|
|
"Choose a different file name for an annotated copy."
|
|
}
|
|
DesktopPdfium.exportAnnotatedPdf(
|
|
document = document,
|
|
destination = target,
|
|
annotations = annotations,
|
|
richTextPageLayouts = richTextPageLayouts
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
internal fun printDesktopPdfDocument(document: DesktopPdfDocument) {
|
|
require(document.formatLabel == "PDF") { "Only PDF files can be printed from the PDF reader." }
|
|
val job = PrinterJob.getPrinterJob()
|
|
job.jobName = "Episteme - ${document.title}"
|
|
val printableBook = Book()
|
|
val pageFormat = job.defaultPage()
|
|
for (pageIndex in 0 until document.pageCount) {
|
|
printableBook.append(
|
|
Printable { graphics, format, _ ->
|
|
drawPdfPageForPrint(document, pageIndex, graphics as Graphics2D, format)
|
|
Printable.PAGE_EXISTS
|
|
},
|
|
pageFormat
|
|
)
|
|
}
|
|
job.setPageable(printableBook)
|
|
if (job.printDialog()) {
|
|
job.print()
|
|
}
|
|
}
|
|
|
|
internal fun buildDesktopPdfRasterOverlays(
|
|
annotations: List<SharedPdfAnnotation>,
|
|
richTextPageLayouts: List<SharedPdfRichPageLayout>,
|
|
pageSizes: List<DesktopPdfPageSize>
|
|
): List<DesktopPdfRasterOverlay> {
|
|
val overlays = mutableListOf<DesktopPdfRasterOverlay>()
|
|
annotations.mapNotNullTo(overlays) { annotation ->
|
|
if (annotation.kind != PdfAnnotationKind.TEXT) return@mapNotNullTo null
|
|
renderDesktopTextBoxOverlay(annotation, pageSizes.getOrNull(annotation.pageIndex) ?: return@mapNotNullTo null)
|
|
}
|
|
richTextPageLayouts.mapNotNullTo(overlays) { layout ->
|
|
renderDesktopRichTextOverlay(layout, pageSizes.getOrNull(layout.pageIndex) ?: return@mapNotNullTo null)
|
|
}
|
|
return overlays
|
|
}
|
|
|
|
private fun drawPdfPageForPrint(
|
|
document: DesktopPdfDocument,
|
|
pageIndex: Int,
|
|
graphics: Graphics2D,
|
|
pageFormat: PageFormat
|
|
) {
|
|
val pageSize = document.pageSizes.getOrNull(pageIndex) ?: return
|
|
val availableWidth = pageFormat.imageableWidth
|
|
val availableHeight = pageFormat.imageableHeight
|
|
val fit = minOf(
|
|
availableWidth / pageSize.width.toDouble(),
|
|
availableHeight / pageSize.height.toDouble()
|
|
).coerceAtLeast(0.01)
|
|
val drawWidth = pageSize.width * fit
|
|
val drawHeight = pageSize.height * fit
|
|
val drawX = pageFormat.imageableX + (availableWidth - drawWidth) / 2.0
|
|
val drawY = pageFormat.imageableY + (availableHeight - drawHeight) / 2.0
|
|
val image = DesktopPdfium.renderPageBufferedImage(document, pageIndex, scale = 2f, renderAnnotations = true)
|
|
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)
|
|
graphics.drawImage(
|
|
image,
|
|
drawX.roundToInt(),
|
|
drawY.roundToInt(),
|
|
drawWidth.roundToInt().coerceAtLeast(1),
|
|
drawHeight.roundToInt().coerceAtLeast(1),
|
|
null
|
|
)
|
|
}
|
|
|
|
private fun renderDesktopTextBoxOverlay(
|
|
annotation: SharedPdfAnnotation,
|
|
pageSize: DesktopPdfPageSize
|
|
): DesktopPdfRasterOverlay? {
|
|
val text = annotation.text.sanitizeDesktopRasterText()
|
|
val bounds = annotation.bounds ?: return null
|
|
if (annotation.pageIndex < 0 || text.isBlank()) return null
|
|
|
|
val left = bounds.left.coerceIn(0f, 1f)
|
|
val top = bounds.top.coerceIn(0f, 1f)
|
|
val right = bounds.right.coerceIn(left, 1f)
|
|
val bottom = bounds.bottom.coerceIn(top, 1f)
|
|
if (right - left <= 0f || bottom - top <= 0f) return null
|
|
|
|
val pageHeightPx = pageSize.exportHeightPx()
|
|
val pageWidthPx = pageHeightPx * pageSize.aspect
|
|
val bitmapWidth = ceil((right - left) * pageWidthPx).toInt().coerceAtLeast(1)
|
|
val bitmapHeight = ceil((bottom - top) * pageHeightPx).toInt().coerceAtLeast(1)
|
|
val paddingPx = DesktopPdfTextBoxPaddingPx
|
|
.coerceAtMost((minOf(bitmapWidth, bitmapHeight) / 2f).coerceAtLeast(0f))
|
|
val contentWidth = (bitmapWidth - paddingPx * 2f).roundToInt().coerceAtLeast(1)
|
|
val fontSizePx = (annotation.sharedPdfTextPageRelativeFontSize() * pageHeightPx).coerceAtLeast(1f)
|
|
val bitmap = BufferedImage(bitmapWidth, bitmapHeight, BufferedImage.TYPE_INT_ARGB)
|
|
|
|
val plainText = AnnotatedString(text)
|
|
val baseStyle = DesktopTextRasterStyle(
|
|
color = annotation.colorArgb.toAwtColor(),
|
|
background = annotation.backgroundArgb.toAwtColor().takeIf { it.alpha > 0 },
|
|
fontSize = fontSizePx,
|
|
isBold = annotation.isBold,
|
|
isItalic = annotation.isItalic,
|
|
isUnderline = annotation.isUnderline,
|
|
isStrikeThrough = annotation.isStrikeThrough,
|
|
fontName = annotation.fontName
|
|
)
|
|
drawDesktopAttributedText(
|
|
bitmap = bitmap,
|
|
text = plainText,
|
|
baseStyle = baseStyle,
|
|
width = contentWidth,
|
|
translateX = paddingPx,
|
|
translateY = paddingPx
|
|
)
|
|
return bitmap.toDesktopRasterOverlay(annotation.pageIndex, left, top, right, bottom)
|
|
}
|
|
|
|
private fun renderDesktopRichTextOverlay(
|
|
layout: SharedPdfRichPageLayout,
|
|
pageSize: DesktopPdfPageSize
|
|
): DesktopPdfRasterOverlay? {
|
|
val visibleText = layout.visibleText.withoutTrailingDesktopPageBreak()
|
|
if (layout.pageIndex < 0 || visibleText.text.isBlank()) return null
|
|
|
|
val pageHeightPx = layout.pageHeightPx.takeIf { it > 0f } ?: pageSize.exportHeightPx()
|
|
val pageWidthPx = pageHeightPx * pageSize.aspect
|
|
val left = DesktopPdfRichTextMarginX
|
|
val top = DesktopPdfRichTextMarginY
|
|
val right = 1f - DesktopPdfRichTextMarginX
|
|
val bottom = 1f - DesktopPdfRichTextMarginY
|
|
val bitmapWidth = ceil((right - left) * pageWidthPx).toInt().coerceAtLeast(1)
|
|
val bitmapHeight = ceil((bottom - top) * pageHeightPx).toInt().coerceAtLeast(1)
|
|
val bitmap = BufferedImage(bitmapWidth, bitmapHeight, BufferedImage.TYPE_INT_ARGB)
|
|
|
|
drawDesktopAttributedText(
|
|
bitmap = bitmap,
|
|
text = visibleText,
|
|
baseStyle = DesktopTextRasterStyle(
|
|
color = java.awt.Color.BLACK,
|
|
background = null,
|
|
fontSize = 16f,
|
|
isBold = false,
|
|
isItalic = false,
|
|
isUnderline = false,
|
|
isStrikeThrough = false,
|
|
fontName = null
|
|
),
|
|
width = bitmapWidth,
|
|
translateX = 0f,
|
|
translateY = 0f
|
|
)
|
|
return bitmap.toDesktopRasterOverlay(layout.pageIndex, left, top, right, bottom)
|
|
}
|
|
|
|
private fun drawDesktopAttributedText(
|
|
bitmap: BufferedImage,
|
|
text: AnnotatedString,
|
|
baseStyle: DesktopTextRasterStyle,
|
|
width: Int,
|
|
translateX: Float,
|
|
translateY: Float
|
|
) {
|
|
val graphics = bitmap.createGraphics()
|
|
try {
|
|
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
|
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
|
graphics.clipRect(0, 0, bitmap.width, bitmap.height)
|
|
var paragraphStart = 0
|
|
var drawY = translateY
|
|
val raw = text.text.sanitizeDesktopRasterTextPreservingLength()
|
|
raw.split('\n').forEach { paragraph ->
|
|
val paragraphEnd = paragraphStart + paragraph.length
|
|
val attributed = attributedParagraph(
|
|
paragraph = paragraph,
|
|
paragraphStart = paragraphStart,
|
|
text = text,
|
|
baseStyle = baseStyle
|
|
)
|
|
val iterator = attributed.iterator
|
|
val measurer = LineBreakMeasurer(iterator, graphics.fontRenderContext)
|
|
while (measurer.position < iterator.endIndex && drawY < bitmap.height) {
|
|
val layout = measurer.nextLayout(width.toFloat())
|
|
drawY += layout.ascent
|
|
layout.draw(graphics, translateX, drawY)
|
|
drawY += layout.descent + layout.leading
|
|
}
|
|
if (paragraph.isEmpty()) {
|
|
drawY += baseStyle.fontSize * 1.2f
|
|
}
|
|
paragraphStart = paragraphEnd + 1
|
|
}
|
|
} finally {
|
|
graphics.dispose()
|
|
}
|
|
}
|
|
|
|
private fun attributedParagraph(
|
|
paragraph: String,
|
|
paragraphStart: Int,
|
|
text: AnnotatedString,
|
|
baseStyle: DesktopTextRasterStyle
|
|
): AttributedString {
|
|
val safeParagraph = paragraph.ifEmpty { " " }
|
|
val attributed = AttributedString(safeParagraph)
|
|
attributed.applyRasterStyle(baseStyle, 0, safeParagraph.length)
|
|
text.spanStyles.forEach { range ->
|
|
val start = maxOf(range.start, paragraphStart) - paragraphStart
|
|
val end = minOf(range.end, paragraphStart + paragraph.length) - paragraphStart
|
|
if (start < end) {
|
|
attributed.applyRasterStyle(range.item.toDesktopTextRasterStyle(baseStyle), start, end)
|
|
}
|
|
}
|
|
return attributed
|
|
}
|
|
|
|
private fun AttributedString.applyRasterStyle(style: DesktopTextRasterStyle, start: Int, end: Int) {
|
|
val safeStart = start.coerceAtLeast(0)
|
|
val safeEnd = end.coerceAtLeast(safeStart)
|
|
if (safeStart >= safeEnd) return
|
|
addAttribute(TextAttribute.FONT, style.awtFont(), safeStart, safeEnd)
|
|
addAttribute(TextAttribute.FOREGROUND, style.color, safeStart, safeEnd)
|
|
style.background?.let { addAttribute(TextAttribute.BACKGROUND, it, safeStart, safeEnd) }
|
|
if (style.isUnderline) {
|
|
addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, safeStart, safeEnd)
|
|
}
|
|
if (style.isStrikeThrough) {
|
|
addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, safeStart, safeEnd)
|
|
}
|
|
}
|
|
|
|
private data class DesktopTextRasterStyle(
|
|
val color: java.awt.Color,
|
|
val background: java.awt.Color?,
|
|
val fontSize: Float,
|
|
val isBold: Boolean,
|
|
val isItalic: Boolean,
|
|
val isUnderline: Boolean,
|
|
val isStrikeThrough: Boolean,
|
|
val fontName: String?
|
|
) {
|
|
fun awtFont(): Font {
|
|
val style = (if (isBold) Font.BOLD else Font.PLAIN) or (if (isItalic) Font.ITALIC else Font.PLAIN)
|
|
return Font(awtFontFamily(fontName), style, fontSize.roundToInt().coerceAtLeast(1))
|
|
}
|
|
}
|
|
|
|
private fun SpanStyle.toDesktopTextRasterStyle(base: DesktopTextRasterStyle): DesktopTextRasterStyle {
|
|
val color = this.color.takeUnless { it == Color.Unspecified }?.toAwtColor() ?: base.color
|
|
val background = this.background.takeUnless { it == Color.Unspecified || it.alpha <= 0f }?.toAwtColor()
|
|
?: base.background
|
|
val fontSize = if (this.fontSize.isSpecified && this.fontSize.isSp) {
|
|
this.fontSize.value
|
|
} else {
|
|
base.fontSize
|
|
}
|
|
val fontWeight = this.fontWeight?.weight ?: if (base.isBold) 700 else 400
|
|
return base.copy(
|
|
color = color,
|
|
background = background,
|
|
fontSize = fontSize,
|
|
isBold = fontWeight >= 600,
|
|
isItalic = this.fontStyle == FontStyle.Italic || base.isItalic,
|
|
isUnderline = this.textDecoration?.contains(TextDecoration.Underline) == true ||
|
|
base.isUnderline,
|
|
isStrikeThrough = this.textDecoration?.contains(TextDecoration.LineThrough) == true ||
|
|
base.isStrikeThrough
|
|
)
|
|
}
|
|
|
|
private fun awtFontFamily(fontName: String?): String {
|
|
return when (fontName?.lowercase()) {
|
|
"serif" -> Font.SERIF
|
|
"monospace" -> Font.MONOSPACED
|
|
else -> Font.SANS_SERIF
|
|
}
|
|
}
|
|
|
|
private fun BufferedImage.toDesktopRasterOverlay(
|
|
pageIndex: Int,
|
|
boundsLeft: Float,
|
|
boundsTop: Float,
|
|
boundsRight: Float,
|
|
boundsBottom: Float
|
|
): DesktopPdfRasterOverlay? {
|
|
val allPixels = IntArray(width * height)
|
|
getRGB(0, 0, width, height, allPixels, 0, width)
|
|
|
|
var minX = width
|
|
var minY = height
|
|
var maxX = -1
|
|
var maxY = -1
|
|
for (y in 0 until height) {
|
|
val rowOffset = y * width
|
|
for (x in 0 until width) {
|
|
if ((allPixels[rowOffset + x] ushr 24) != 0) {
|
|
if (x < minX) minX = x
|
|
if (x > maxX) maxX = x
|
|
if (y < minY) minY = y
|
|
if (y > maxY) maxY = y
|
|
}
|
|
}
|
|
}
|
|
if (maxX < minX || maxY < minY) return null
|
|
|
|
val cropWidth = maxX - minX + 1
|
|
val cropHeight = maxY - minY + 1
|
|
val cropped = IntArray(cropWidth * cropHeight)
|
|
for (row in 0 until cropHeight) {
|
|
System.arraycopy(
|
|
allPixels,
|
|
(minY + row) * width + minX,
|
|
cropped,
|
|
row * cropWidth,
|
|
cropWidth
|
|
)
|
|
}
|
|
|
|
val boundsWidth = boundsRight - boundsLeft
|
|
val boundsHeight = boundsBottom - boundsTop
|
|
return DesktopPdfRasterOverlay(
|
|
pageIndex = pageIndex,
|
|
left = boundsLeft + boundsWidth * (minX.toFloat() / width),
|
|
top = boundsTop + boundsHeight * (minY.toFloat() / height),
|
|
right = boundsLeft + boundsWidth * ((maxX + 1).toFloat() / width),
|
|
bottom = boundsTop + boundsHeight * ((maxY + 1).toFloat() / height),
|
|
width = cropWidth,
|
|
height = cropHeight,
|
|
pixels = cropped
|
|
)
|
|
}
|
|
|
|
private val DesktopPdfPageSize.aspect: Float
|
|
get() = if (width > 0f && height > 0f) width / height else 612f / 792f
|
|
|
|
private fun DesktopPdfPageSize.exportHeightPx(): Float {
|
|
return (height * DesktopPdfTextRasterPointScale)
|
|
.coerceIn(DesktopPdfTextRasterMinPageHeightPx, DesktopPdfTextRasterMaxPageHeightPx)
|
|
}
|
|
|
|
private fun AnnotatedString.withoutTrailingDesktopPageBreak(): AnnotatedString =
|
|
if (text.lastOrNull() == SHARED_PDF_PAGE_BREAK_CHAR) subSequence(0, length - 1) else this
|
|
|
|
private fun String.sanitizeDesktopRasterText(): String =
|
|
replace(SHARED_PDF_PAGE_BREAK_CHAR, '\n')
|
|
.replace("\u200B", "")
|
|
.replace('\r', ' ')
|
|
|
|
private fun String.sanitizeDesktopRasterTextPreservingLength(): String =
|
|
replace(SHARED_PDF_PAGE_BREAK_CHAR, '\n')
|
|
.replace('\r', ' ')
|
|
|
|
private fun Int.toAwtColor(): java.awt.Color = java.awt.Color(this, true)
|
|
|
|
private fun Color.toAwtColor(): java.awt.Color = java.awt.Color(toArgb(), true)
|