book-reader/app/src/test/java/com/aryan/reader/EmbeddedEbookMetadataExtractorTest.kt
Aryan b20ade9946
Desktop app (#308)
* Implement build profiles and feature policy for offline desktop builds

* Introduce unified cross-platform Settings Hub

* Refactor main settings into a hierarchical page-based navigation model

* Refactor library projection to use shared multiplatform logic

* Refactor UI state consumption by removing intermediate screen models

* Introduce AndroidSharedStateBridge to centralize state mapping and reduction logic

* Refactor state management for tabs, selection, and pinning to use shared bridge logic

* Refactor file type management and validation into a centralized shared module

* Centralize file type resolution and improve handling of unknown types

* Centralize book import logic with SharedImportPlanner

* Refactor magnifier geometry logic and coordinate mapping

* Properly handle orientation changes in scroll-locked PDF reader

* Add screen orientation controls to EPUB and PDF readers

* Implement right-to-left (RTL) pagination support and refactor reader menus

* Separate right-to-left pagination settings for PDF and EPUB

* Ensure PDF page data is scoped by document key for multi tab support

* Implement theme-aware link styling for the epub reader

* Implement jump history for back and forward navigation in the epub reader

* Improve locator handling and navigation logic in paginated reader mode

* Implement stable pagination navigation and location tracking

* Centralize banner message management and auto-dismiss logic in MainViewModel

* Implement zoom and pan state preservation for PDF pan lock mode

* Enhance reader navigation UI and workspace layout management in desktop app

* Refactor reader navigation sidebar and relocate search controls in desktop app

* Enhance reader UI with redesigned selection menus and bottom sheet overlays

* Implement custom highlight palettes and reader theme customization in desktop app

* Implement cross-platform modal layer and refine reader UI styling

* Improve highlight accuracy and implement metadata enrichment on book open in desktop app

* Implement two-page spread layout for paginated reader on desktop

* Implement persistent caching for book loading and pagination in desktop app

* Implement persistent caching for book loading and pagination in desktop app

* Optimize reader settings updates by separating layout and appearance changes in desktop app

* Improve desktop window branding and native Windows styling

* Enhance reader selection interactions and UI across EPUB and PDF viewers in desktop app

* Refine selection handle positioning and interaction logic

* Implement EPUB selection debug logging and improve handle targeting

* Optimize desktop book loading performance and UI responsiveness

* Implement anchored zoom gestures and rendering optimizations for the Desktop PDF viewer.

* Implement smooth zoom preview for the PDF reader in desktop app

* Optimize PDF rendering performance and responsiveness in the desktop reader

* Implement conditional diagnostic logging and update desktop build configuration

* Implemented hierarchical TOC, custom scrollbars, and improved desktop modal handling

* Added management options for annotations and highlights in the sidebar in desktop app

* Implemented `SharedStableOutlinedTextField` and updated text input fields to use `TextFieldValue` for improved cursor and selection stability.

* Refined library filters and enhanced OPDS functionality in desktop app

* Improved EPUB pagination measurement and implemented layout diagnostic logging for desktop app

* Added PPTX support including document parsing, rendering, and indexing

* Improved PPTX rendering and layout accuracy

* Implemented text autofit support for PPTX rendering

* Enhanced PPTX rendering with support for custom geometry, automatic numbering, table styles, and image opacity

* Improved EPUB pagination accuracy and added layout telemetry in desktop app

* Improved folder synchronization with metadata-only mode and hashed sidecar management in desktop app

* Implemented rich text font scaling and migrated desktop ink tools to custom pointer input handling

* Implemented billing account obfuscation

* Implemented hierarchical folder navigation and improved library selection functionality in desktop app

* Implemented platform-aware directory resolution and multi-platform native library support for desktop

* Added full-screen mode for the reader workspace

* Added PDF zoom indicator and interactive vertical scrollbar with page tooltips

* Refactored speech bubble prefetching to use a limited radius and improved ML detector initialization and lifecycle management

* Updated PDF indexing to replace existing page text and removed search result item keys

* Implemented "preparing" foreground notification for TTS service

* Optimized PDF rendering performance by pre-calculating page-specific annotations

* Refactored desktop packaging tasks and improved distribution configuration

* Optimized EPUB parser memory usage and added path traversal protection

* Refactored WorkManager monitoring logic and added work pruning

* Implemented comprehensive resource cleanup and memory management for WebView-based components to prevent memory leaks

* Implemented bitmap size limits and scaling to prevent canvas rendering errors

* Split long text paragraphs into multiple semantic blocks during HTML parsing

* Implemented local ActionMode for text selection to prevent platform crashes

* Refactored PPTX text layout, optimized HtmlParser block detection, and improved banner dismissal logic

* Added desktop startup splash screen and deferred WebView initialization

* Reorganized settings hub and added separate PDF reader defaults

* Implemented embedded cover extraction and metadata support for MOBI and FB2 formats

* Implemented batching for MetadataExtractionWorker and optimized EPUB metadata extraction performance.

* Implemented procedurally generated book covers and replaced static placeholders

* Redesigned search UI with a top bar and results overlay in desktop app

* Added PDF page gap and overlay visibility options and implemented DesktopBookImporter

* Refactored PDF reader UI with tabbed inspector and improved theme background handling in desktop

* Implemented PDF viewport persistence for zoom and scroll positions in desktop app

* Improved desktop fullscreen implementation and state restoration

* Implemented desktop window state persistence

* Implemented flavor-based branding and ProGuard configuration for desktop builds

* Implemented precise reader positioning and improved highlight rendering logic in desktop app

* Added support for user-editable book metadata

* Enhanced book metadata support and integrated info/edit dialogs

* Implemented embedded EPUB metadata editing

* Improved highlight mapping and added custom scrollbar styling for the reader.

* Reduced desktop WebView bundle size by excluding unused locales and runtime files

* Added neutral pan mode as the default PDF interaction state.

* Refactored library empty states and updated primary navigation tabs in desktop app

* Implemented native paginated reader and unified content rendering architecture in desktop epub reader

* Implemented native EPUB image rendering for desktop and improved block layout spacing with margin collapsing.

* Improved pagination overflow detection in desktop

* Implemented multi-block text selection with interactive handles and CFI support in desktop epub pagination
2026-05-15 22:36:51 +05:30

215 lines
8.1 KiB
Kotlin

package com.aryan.reader
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.Base64
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@RunWith(RobolectricTestRunner::class)
class EmbeddedEbookMetadataExtractorTest {
@Test
fun `epub extracts text metadata and explicitly referenced cover image`() {
val coverBytes = onePixelPngBytes()
val epubBytes = zipBytes(
"META-INF/container.xml" to """
<container>
<rootfiles>
<rootfile full-path="OEBPS/content.opf"/>
</rootfiles>
</container>
""".trimIndent().toByteArray(Charsets.UTF_8),
"OEBPS/content.opf" to """
<package xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata>
<dc:title>Folder EPUB</dc:title>
<dc:creator>Octavia Butler</dc:creator>
<dc:description>&lt;p&gt;Folder summary&lt;/p&gt;</dc:description>
<meta content="Patternist" name="calibre:series"/>
<meta content="3" name="calibre:series_index"/>
<meta name="cover" content="cover-image"/>
</metadata>
<manifest>
<item id="cover-image" href="images/cover.png" media-type="image/png"/>
</manifest>
</package>
""".trimIndent().toByteArray(Charsets.UTF_8),
"OEBPS/images/cover.png" to coverBytes
)
val metadata = EmbeddedEbookMetadataExtractor.extract(
type = FileType.EPUB,
displayName = "folder.epub",
openStream = { ByteArrayInputStream(epubBytes) }
)
assertEquals("Folder EPUB", metadata.title)
assertEquals("Octavia Butler", metadata.author)
assertEquals("<p>Folder summary</p>", metadata.description)
assertEquals("Patternist", metadata.seriesName)
assertEquals(3.0, metadata.seriesIndex)
val cover = metadata.cover
assertNotNull(cover)
assertEquals("png", cover!!.extension)
assertArrayEquals(coverBytes, cover.bytes)
}
@Test
fun `fb2 extracts coverpage binary without parsing book body`() {
val coverBytes = onePixelPngBytes()
val fb2 = """
<FictionBook xmlns:l="http://www.w3.org/1999/xlink">
<description>
<title-info>
<book-title>Folder FB2</book-title>
<author>
<first-name>Ursula</first-name>
<last-name>Le Guin</last-name>
</author>
<annotation><image l:href="#not-cover.png"/></annotation>
<coverpage><image l:href="#cover.png"/></coverpage>
</title-info>
</description>
<body><section><p>Body text should not matter.</p></section></body>
<binary id="not-cover.png" content-type="image/png">${Base64.getEncoder().encodeToString(ByteArray(0))}</binary>
<binary id="cover.png" content-type="image/png">${Base64.getEncoder().encodeToString(coverBytes)}</binary>
</FictionBook>
""".trimIndent()
val metadata = EmbeddedEbookMetadataExtractor.extract(
type = FileType.FB2,
displayName = "folder.fb2",
openStream = { ByteArrayInputStream(fb2.toByteArray(Charsets.UTF_8)) }
)
assertEquals("Folder FB2", metadata.title)
assertEquals("Ursula Le Guin", metadata.author)
val cover = metadata.cover
assertNotNull(cover)
assertEquals("png", cover!!.extension)
assertArrayEquals(coverBytes, cover.bytes)
}
@Test
fun `mobi extracts EXTH text metadata and embedded cover record`() {
val coverBytes = onePixelPngBytes()
val mobiBytes = minimalMobiBytes(
title = "Folder MOBI",
author = "N K Jemisin",
coverBytes = coverBytes
)
val metadata = EmbeddedEbookMetadataExtractor.extract(
type = FileType.MOBI,
displayName = "folder.mobi",
openStream = { ByteArrayInputStream(mobiBytes) }
)
assertEquals("Folder MOBI", metadata.title)
assertEquals("N K Jemisin", metadata.author)
val cover = metadata.cover
assertNotNull(cover)
assertEquals("png", cover!!.extension)
assertArrayEquals(coverBytes, cover.bytes)
}
private fun zipBytes(vararg entries: Pair<String, ByteArray>): ByteArray {
val out = ByteArrayOutputStream()
ZipOutputStream(out).use { zip ->
entries.forEach { (name, content) ->
zip.putNextEntry(ZipEntry(name))
zip.write(content)
zip.closeEntry()
}
}
return out.toByteArray()
}
private fun minimalMobiBytes(title: String, author: String, coverBytes: ByteArray): ByteArray {
val exthRecords = listOf(
exthStringRecord(99, title),
exthStringRecord(100, author),
exthIntRecord(201, 0)
)
val exthSize = 12 + exthRecords.sumOf { it.size }
val mobiHeaderLength = 232
val record0 = ByteArray(16 + mobiHeaderLength + exthSize)
putU16(record0, 0, 1)
putU32(record0, 4, 0)
putU16(record0, 8, 0)
putU16(record0, 12, 0)
putAscii(record0, 16, "MOBI")
putU32(record0, 20, mobiHeaderLength)
putU32(record0, 16 + 12, 65001)
putU32(record0, 16 + 68, 0)
putU32(record0, 16 + 72, 0)
putU32(record0, 16 + 92, 1)
val exthOffset = 16 + mobiHeaderLength
putAscii(record0, exthOffset, "EXTH")
putU32(record0, exthOffset + 4, exthSize)
putU32(record0, exthOffset + 8, exthRecords.size)
var cursor = exthOffset + 12
exthRecords.forEach { record ->
record.copyInto(record0, cursor)
cursor += record.size
}
val palmHeader = ByteArray(78 + 8 * 2)
putU16(palmHeader, 76, 2)
val record0Offset = palmHeader.size
val coverOffset = record0Offset + record0.size
putU32(palmHeader, 78, record0Offset)
putU32(palmHeader, 86, coverOffset)
return palmHeader + record0 + coverBytes
}
private fun exthStringRecord(type: Int, value: String): ByteArray {
val data = value.toByteArray(Charsets.UTF_8)
return exthRecord(type, data)
}
private fun exthIntRecord(type: Int, value: Int): ByteArray {
val data = ByteArray(4)
putU32(data, 0, value)
return exthRecord(type, data)
}
private fun exthRecord(type: Int, data: ByteArray): ByteArray {
val record = ByteArray(8 + data.size)
putU32(record, 0, type)
putU32(record, 4, record.size)
data.copyInto(record, 8)
return record
}
private fun putAscii(target: ByteArray, offset: Int, value: String) {
value.toByteArray(Charsets.US_ASCII).copyInto(target, offset)
}
private fun putU16(target: ByteArray, offset: Int, value: Int) {
target[offset] = ((value ushr 8) and 0xFF).toByte()
target[offset + 1] = (value and 0xFF).toByte()
}
private fun putU32(target: ByteArray, offset: Int, value: Int) {
target[offset] = ((value ushr 24) and 0xFF).toByte()
target[offset + 1] = ((value ushr 16) and 0xFF).toByte()
target[offset + 2] = ((value ushr 8) and 0xFF).toByte()
target[offset + 3] = (value and 0xFF).toByte()
}
private fun onePixelPngBytes(): ByteArray {
return Base64.getDecoder().decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="
)
}
}