Update v1.0.49 (#330)
* 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
This commit is contained in:
parent
dc5196526f
commit
9510293ac3
245 changed files with 37538 additions and 12460 deletions
|
|
@ -0,0 +1,500 @@
|
|||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue