/* * Episteme Reader - A native Android document reader. * Copyright (C) 2026 Episteme * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * mail: epistemereader@gmail.com */ @file:OptIn(ExperimentalSerializationApi::class) package com.aryan.reader.paginatedreader import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.isSpecified import com.aryan.reader.paginatedreader.serialization.AnnotatedStringSerializer import com.aryan.reader.paginatedreader.serialization.ColorSerializer import com.aryan.reader.paginatedreader.serialization.DpSerializer import com.aryan.reader.paginatedreader.serialization.ParagraphStyleSerializer import com.aryan.reader.paginatedreader.serialization.SpanStyleSerializer import com.aryan.reader.paginatedreader.serialization.TextAlignSerializer import com.aryan.reader.paginatedreader.serialization.TextUnitSerializer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber @Serializable data class BlockStyle( @ProtoNumber(1) val margin: BoxBorders = BoxBorders(), @ProtoNumber(2) val padding: BoxBorders = BoxBorders(), @ProtoNumber(3) @Serializable(with = DpSerializer::class) val width: Dp = Dp.Unspecified, @ProtoNumber(4) @Serializable(with = DpSerializer::class) val maxWidth: Dp = Dp.Unspecified, @ProtoNumber(5) @Serializable(with = DpSerializer::class) val height: Dp = Dp.Unspecified, @ProtoNumber(6) @Serializable(with = ColorSerializer::class) val backgroundColor: Color = Color.Unspecified, @ProtoNumber(7) val borderTop: BorderStyle? = null, @ProtoNumber(8) val borderRight: BorderStyle? = null, @ProtoNumber(9) val borderBottom: BorderStyle? = null, @ProtoNumber(10) val borderLeft: BorderStyle? = null, @ProtoNumber(11) val listStyleType: String? = null, @ProtoNumber(12) val listStyleImage: String? = null, @ProtoNumber(13) val pageBreakInsideAvoid: Boolean = false, @ProtoNumber(14) val pageBreakAfterAvoid: Boolean = false, @ProtoNumber(15) val boxSizing: String? = null, @ProtoNumber(16) val float: String? = null, @ProtoNumber(17) val clear: String? = null, @ProtoNumber(18) val position: String? = null, @ProtoNumber(19) @Serializable(with = DpSerializer::class) val top: Dp = Dp.Unspecified, @ProtoNumber(20) @Serializable(with = DpSerializer::class) val right: Dp = Dp.Unspecified, @ProtoNumber(21) @Serializable(with = DpSerializer::class) val bottom: Dp = Dp.Unspecified, @ProtoNumber(22) @Serializable(with = DpSerializer::class) val left: Dp = Dp.Unspecified, @ProtoNumber(23) val display: String? = null, @ProtoNumber(24) val flexDirection: String? = null, @ProtoNumber(25) val justifyContent: String? = null, @ProtoNumber(26) val alignItems: String? = null, @ProtoNumber(27) val horizontalAlign: String? = null, @ProtoNumber(28) val filter: String? = null, @ProtoNumber(29) val borderCollapse: String? = null, @ProtoNumber(30) @Serializable(with = DpSerializer::class) val borderTopLeftRadius: Dp = 0.dp, @ProtoNumber(31) @Serializable(with = DpSerializer::class) val borderTopRightRadius: Dp = 0.dp, @ProtoNumber(32) @Serializable(with = DpSerializer::class) val borderBottomRightRadius: Dp = 0.dp, @ProtoNumber(33) @Serializable(with = DpSerializer::class) val borderBottomLeftRadius: Dp = 0.dp, @ProtoNumber(34) @Serializable(with = DpSerializer::class) val borderSpacing: Dp = 0.dp ) { fun merge(other: BlockStyle): BlockStyle { return BlockStyle( margin = BoxBorders( top = if (other.margin.top != 0.dp) other.margin.top else this.margin.top, bottom = if (other.margin.bottom != 0.dp) other.margin.bottom else this.margin.bottom, left = if (other.margin.left != 0.dp) other.margin.left else this.margin.left, right = if (other.margin.right != 0.dp) other.margin.right else this.margin.right ), padding = BoxBorders( top = if (other.padding.top != 0.dp) other.padding.top else this.padding.top, bottom = if (other.padding.bottom != 0.dp) other.padding.bottom else this.padding.bottom, left = if (other.padding.left != 0.dp) other.padding.left else this.padding.left, right = if (other.padding.right != 0.dp) other.padding.right else this.padding.right ), width = if (other.width != Dp.Unspecified) other.width else this.width, maxWidth = if (other.maxWidth != Dp.Unspecified) other.maxWidth else this.maxWidth, height = if (other.height != Dp.Unspecified) other.height else this.height, backgroundColor = if (other.backgroundColor.isSpecified) other.backgroundColor else this.backgroundColor, borderTop = other.borderTop ?: this.borderTop, borderRight = other.borderRight ?: this.borderRight, borderBottom = other.borderBottom ?: this.borderBottom, borderLeft = other.borderLeft ?: this.borderLeft, borderTopLeftRadius = if (other.borderTopLeftRadius != 0.dp) other.borderTopLeftRadius else this.borderTopLeftRadius, borderTopRightRadius = if (other.borderTopRightRadius != 0.dp) other.borderTopRightRadius else this.borderTopRightRadius, borderBottomRightRadius = if (other.borderBottomRightRadius != 0.dp) other.borderBottomRightRadius else this.borderBottomRightRadius, borderBottomLeftRadius = if (other.borderBottomLeftRadius != 0.dp) other.borderBottomLeftRadius else this.borderBottomLeftRadius, listStyleType = other.listStyleType ?: this.listStyleType, listStyleImage = other.listStyleImage ?: this.listStyleImage, pageBreakInsideAvoid = this.pageBreakInsideAvoid || other.pageBreakInsideAvoid, pageBreakAfterAvoid = this.pageBreakAfterAvoid || other.pageBreakAfterAvoid, boxSizing = other.boxSizing ?: this.boxSizing, float = other.float ?: this.float, clear = other.clear ?: this.clear, position = other.position ?: this.position, top = if (other.top.isSpecified) other.top else this.top, right = if (other.right.isSpecified) other.right else this.right, bottom = if (other.bottom.isSpecified) other.bottom else this.bottom, left = if (other.left.isSpecified) other.left else this.left, display = other.display ?: this.display, flexDirection = other.flexDirection ?: this.flexDirection, justifyContent = other.justifyContent ?: this.justifyContent, alignItems = other.alignItems ?: this.alignItems, horizontalAlign = other.horizontalAlign ?: this.horizontalAlign, filter = other.filter ?: this.filter, borderCollapse = other.borderCollapse ?: this.borderCollapse, borderSpacing = if (other.borderSpacing != 0.dp) other.borderSpacing else this.borderSpacing ) } } @Serializable data class BoxBorders( @ProtoNumber(1) @Serializable(with = DpSerializer::class) val top: Dp = 0.dp, @ProtoNumber(2) @Serializable(with = DpSerializer::class) val right: Dp = 0.dp, @ProtoNumber(3) @Serializable(with = DpSerializer::class) val bottom: Dp = 0.dp, @ProtoNumber(4) @Serializable(with = DpSerializer::class) val left: Dp = 0.dp ) @Serializable data class BorderStyle( @ProtoNumber(1) @Serializable(with = DpSerializer::class) val width: Dp = 0.dp, @ProtoNumber(2) @Serializable(with = ColorSerializer::class) val color: Color = Color.Transparent, @ProtoNumber(3) val style: String = "solid" ) @Serializable sealed interface ContentBlock { val style: BlockStyle val elementId: String? val cfi: String? val blockIndex: Int val expectedHeight: Int } sealed interface TextContentBlock : ContentBlock { val content: AnnotatedString val startCharOffsetInSource: Int val endCharOffsetInSource: Int } @Serializable data class ParagraphBlock( @ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString, @ProtoNumber(2) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null, @ProtoNumber(3) override val style: BlockStyle = BlockStyle(), @ProtoNumber(4) override val elementId: String? = null, @ProtoNumber(5) override val cfi: String? = null, @ProtoNumber(6) override val startCharOffsetInSource: Int = 0, @ProtoNumber(7) override val endCharOffsetInSource: Int = -1, @ProtoNumber(8) override val blockIndex: Int, @ProtoNumber(9) override val expectedHeight: Int = 0 ) : TextContentBlock @Serializable data class ImageBlock( @ProtoNumber(1) val path: String, @ProtoNumber(2) val altText: String?, @ProtoNumber(3) val intrinsicWidth: Float? = null, @ProtoNumber(4) val intrinsicHeight: Float? = null, @ProtoNumber(5) override val style: BlockStyle = BlockStyle(), @ProtoNumber(6) override val elementId: String? = null, @ProtoNumber(7) override val cfi: String? = null, @ProtoNumber(8) val invertOnDarkTheme: Boolean = false, @ProtoNumber(9) override val blockIndex: Int, @ProtoNumber(10) override val expectedHeight: Int = 0 ) : ContentBlock @Serializable data class HeaderBlock( @ProtoNumber(1) val level: Int, @ProtoNumber(2) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString, @ProtoNumber(3) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null, @ProtoNumber(4) override val style: BlockStyle = BlockStyle(), @ProtoNumber(5) override val elementId: String? = null, @ProtoNumber(6) override val cfi: String? = null, @ProtoNumber(7) override val startCharOffsetInSource: Int = 0, @ProtoNumber(8) override val endCharOffsetInSource: Int = -1, @ProtoNumber(9) override val blockIndex: Int, @ProtoNumber(10) override val expectedHeight: Int = 0 ) : TextContentBlock @Serializable data class SpacerBlock( @ProtoNumber(1) @Serializable(with = DpSerializer::class) val height: Dp = 8.dp, @ProtoNumber(2) override val style: BlockStyle = BlockStyle(), @ProtoNumber(3) override val elementId: String? = null, @ProtoNumber(4) override val cfi: String? = null, @ProtoNumber(5) override val blockIndex: Int, @ProtoNumber(6) override val expectedHeight: Int = 0 ) : ContentBlock @Serializable data class QuoteBlock( @ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString, @ProtoNumber(2) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null, @ProtoNumber(3) override val style: BlockStyle = BlockStyle(), @ProtoNumber(4) override val elementId: String? = null, @ProtoNumber(5) override val cfi: String? = null, @ProtoNumber(6) override val startCharOffsetInSource: Int = 0, @ProtoNumber(7) override val endCharOffsetInSource: Int = -1, @ProtoNumber(8) override val blockIndex: Int, @ProtoNumber(9) override val expectedHeight: Int = 0 ) : TextContentBlock @Serializable data class ListItemBlock( @ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString, @ProtoNumber(2) val itemMarker: String?, @ProtoNumber(3) val itemMarkerImage: String? = null, @ProtoNumber(4) override val style: BlockStyle, @ProtoNumber(5) override val elementId: String? = null, @ProtoNumber(6) override val cfi: String? = null, @ProtoNumber(7) override val startCharOffsetInSource: Int = 0, @ProtoNumber(8) override val endCharOffsetInSource: Int = -1, @ProtoNumber(9) override val blockIndex: Int, @ProtoNumber(10) override val expectedHeight: Int = 0 ) : TextContentBlock @Serializable data class TableCell( @ProtoNumber(1) val content: List, @ProtoNumber(2) val isHeader: Boolean = false, @ProtoNumber(3) val style: CssStyle = CssStyle(), @ProtoNumber(4) val colspan: Int = 1 ) @Serializable data class TableBlock( @ProtoNumber(1) val rows: List>, @ProtoNumber(2) override val style: BlockStyle = BlockStyle(), @ProtoNumber(3) override val elementId: String? = null, @ProtoNumber(4) override val cfi: String? = null, @ProtoNumber(5) override val blockIndex: Int, @ProtoNumber(6) override val expectedHeight: Int = 0 ) : ContentBlock @Serializable data class MathBlock( @ProtoNumber(1) val svgContent: String?, @ProtoNumber(2) val altText: String?, @ProtoNumber(3) override val style: BlockStyle, @ProtoNumber(4) override val elementId: String?, @ProtoNumber(5) override val cfi: String?, @ProtoNumber(6) val svgWidth: String? = null, @ProtoNumber(7) val svgHeight: String? = null, @ProtoNumber(8) val svgViewBox: String? = null, @ProtoNumber(9) val isFromMathJax: Boolean = false, @ProtoNumber(10) override val blockIndex: Int, @ProtoNumber(11) override val expectedHeight: Int = 0 ) : ContentBlock @Serializable data class WrappingContentBlock( @ProtoNumber(1) val floatedImage: ImageBlock, @ProtoNumber(2) val paragraphsToWrap: List, @ProtoNumber(3) override val style: BlockStyle = BlockStyle(), @ProtoNumber(4) override val elementId: String? = null, @ProtoNumber(5) override val cfi: String? = null, @ProtoNumber(6) override val blockIndex: Int, @ProtoNumber(7) override val expectedHeight: Int = 0 ) : ContentBlock @Serializable data class TextEmphasis( @ProtoNumber(1) val style: String? = null, @ProtoNumber(2) val fill: String? = null, @ProtoNumber(3) @Serializable(with = ColorSerializer::class) val color: Color = Color.Unspecified, @ProtoNumber(4) val position: String? = null ) @Serializable data class CssStyle( @ProtoNumber(1) @Serializable(with = SpanStyleSerializer::class) val spanStyle: SpanStyle = SpanStyle(), @ProtoNumber(2) @Serializable(with = ParagraphStyleSerializer::class) val paragraphStyle: ParagraphStyle = ParagraphStyle(), @ProtoNumber(3) val blockStyle: BlockStyle = BlockStyle(), @ProtoNumber(4) val fontFamilies: List = emptyList(), @ProtoNumber(5) val display: String? = null, @ProtoNumber(6) @Serializable(with = TextUnitSerializer::class) val fontSize: TextUnit = TextUnit.Unspecified, @ProtoNumber(7) val textTransform: String? = null, @ProtoNumber(8) val boxSizing: String? = null, @ProtoNumber(9) val content: String? = null, @ProtoNumber(10) val hyphens: String? = null, @ProtoNumber(11) val fontVariantNumeric: String? = null, @ProtoNumber(12) val textEmphasis: TextEmphasis? = null, @ProtoNumber(13) @Serializable(with = TextUnitSerializer::class) val wordSpacing: TextUnit = TextUnit.Unspecified, @ProtoNumber(14) val textDecorationStyle: String? = null, @ProtoNumber(15) @Serializable(with = ColorSerializer::class) val textDecorationColor: Color = Color.Unspecified, @ProtoNumber(16) @Serializable(with = DpSerializer::class) val textUnderlineOffset: Dp = Dp.Unspecified ) { fun merge(other: CssStyle): CssStyle { return CssStyle( spanStyle = this.spanStyle.merge(other.spanStyle), paragraphStyle = this.paragraphStyle.merge(other.paragraphStyle), blockStyle = this.blockStyle.merge(other.blockStyle), fontFamilies = other.fontFamilies.takeIf { it.isNotEmpty() } ?: this.fontFamilies, display = other.display ?: this.display, fontSize = if (other.fontSize.isSpecified) other.fontSize else this.fontSize, textTransform = other.textTransform ?: this.textTransform, boxSizing = other.boxSizing ?: this.boxSizing, content = other.content ?: this.content, hyphens = other.hyphens ?: this.hyphens, fontVariantNumeric = other.fontVariantNumeric ?: this.fontVariantNumeric, textEmphasis = other.textEmphasis ?: this.textEmphasis, wordSpacing = if (other.wordSpacing.isSpecified) other.wordSpacing else this.wordSpacing, textDecorationStyle = other.textDecorationStyle ?: this.textDecorationStyle, textDecorationColor = if (other.textDecorationColor.isSpecified) other.textDecorationColor else this.textDecorationColor, textUnderlineOffset = if (other.textUnderlineOffset.isSpecified) other.textUnderlineOffset else this.textUnderlineOffset ) } } @Serializable data class CssSelector( @ProtoNumber(1) val selector: String, @ProtoNumber(2) val specificity: Int ) @Serializable data class CssRule( @ProtoNumber(1) val selector: CssSelector, @ProtoNumber(2) val style: CssStyle ) @Serializable data class FontFaceInfo( @ProtoNumber(1) val fontFamily: String, @ProtoNumber(2) val src: String, @ProtoNumber(3) @Serializable(with = com.aryan.reader.paginatedreader.serialization.FontWeightSerializer::class) val fontWeight: FontWeight?, @ProtoNumber(4) @Serializable(with = com.aryan.reader.paginatedreader.serialization.FontStyleSerializer::class) val fontStyle: FontStyle? ) @Serializable data class Page( @ProtoNumber(1) val content: List ) @Serializable data class FlexContainerBlock( @ProtoNumber(1) val children: List, @ProtoNumber(2) override val style: BlockStyle = BlockStyle(), @ProtoNumber(3) override val elementId: String? = null, @ProtoNumber(4) override val cfi: String? = null, @ProtoNumber(5) override val blockIndex: Int, @ProtoNumber(6) override val expectedHeight: Int = 0 ) : ContentBlock @Serializable data class OptimizedCssRules( @ProtoNumber(1) val byTag: Map> = emptyMap(), @ProtoNumber(2) val byClass: Map> = emptyMap(), @ProtoNumber(3) val byId: Map> = emptyMap(), @ProtoNumber(4) val otherComplex: List = emptyList() ) { fun merge(other: OptimizedCssRules): OptimizedCssRules { fun mergeMap( m1: Map>, m2: Map> ): Map> { if (m1.isEmpty()) return m2 if (m2.isEmpty()) return m1 val result = LinkedHashMap(m1) for ((key, value) in m2) { val existing = result[key] if (existing != null) { result[key] = existing + value } else { result[key] = value } } return result } return OptimizedCssRules( byTag = mergeMap(this.byTag, other.byTag), byClass = mergeMap(this.byClass, other.byClass), byId = mergeMap(this.byId, other.byId), otherComplex = this.otherComplex + other.otherComplex ) } fun toFlatList(): List { return byTag.values.flatten() + byClass.values.flatten() + byId.values.flatten() + otherComplex } } data class OptimizedCssParseResult( val rules: OptimizedCssRules, val fontFaces: List )