PDF text, outline, and annotation core library for Fileloom.
Coordinates:
implementation("dev.jaeyoung:fileloom-pdf-core:0.2.0")Provide a pure Kotlin/JVM layer that consumes a PDF and exposes reader-focused metadata, so Fileloom can:
- Surface PDF text to TalkBack (Android Screen Reader) via Compose semantics overlays on top of the existing
PdfRendererbitmap. - Feed the EPUB TTS engine arbitrary
Stringcontent for "read aloud" on PDFs. - Show PDF outline trees as a table of contents.
- Export Fileloom's persisted highlights and sticky notes as standard PDF annotations.
This remains intentionally small: Fileloom still renders pages with Android PdfRenderer, while this library handles the metadata and write-back paths that the platform renderer does not expose consistently.
In scope:
- Page tree walking (handles inherited
MediaBox,CropBox,Rotate, nestedKids). - Content-stream extraction (find
stream...endstream, apply/Filterpipeline). - Filter pipeline:
FlateDecode(zlib),ASCIIHexDecode,ASCII85Decode. (LZW deferred — uncommon in modern PDFs.) - Content-stream tokenizer + text operators:
Tj,TJ,',",Tf,Tm,Td,TD,T*,Tw,Tc,TL,BT,ET. - Font dictionary parsing:
Type0,Type1,TrueType,MMType1(treated like Type1). - ToUnicode CMap parser (
bfchar+bfrangeentries). This is the load-bearing piece — without it, glyph codes don't map back to readable Unicode. - Standard-14 encoding fallback (
WinAnsiEncoding,MacRomanEncoding,StandardEncoding). - Heuristic reading-order: top-to-bottom by Y, left-to-right by X within a line band.
- Outline extraction: walks
/Outlineslinked lists, preserves nested children, and resolves direct page destinations to zero-based page indexes. - Incremental annotation export: appends Highlight and Text/sticky-note annotation objects, updates page
/Annots, preserves the original bytes, and links the new xref to the previous one with/Prev.
Explicitly out of scope for v0.2:
- Encrypted PDFs (use
dev.jaeyoung:fileloom-pdf-security-corefirst to decrypt to a plaintext temp file). - Cross-reference streams (PDF 1.5+ binary xref). The dependency
fileloom-pdf-parser-core:0.3.0only handles classic xref tables; PDFs that use xref streams will be skipped with a graceful empty result. - LZW/CCITT/JBIG2/DCT filters.
- Embedded-font glyph rendering (we only need glyph code -> Unicode, never code -> shape).
- OCR for scanned/image-only PDFs.
- Text formatting preservation (paragraphs, columns, tables). Output is plain text in approximate reading order.
- Full annotation editing, ink/stylus pressure, underline/strikethrough export, form filling, and optional-content/layer browser support.
Per the Fileloom in-house library policy, the design borrows from these reference implementations (each used for learning, not copy-paste):
| Rank | Library | Lang | License | Notes on what was studied |
|---|---|---|---|---|
| 1 | Apache PDFBox | Java | Apache 2.0 | org.apache.pdfbox.contentstream.PDFStreamEngine for the operator-dispatcher pattern. Text-extraction utility (PDFTextStripper) for line-bucketing logic. PDFBox is overkill for our scope but its operator table maps 1:1 to the spec. |
| 2 | pdf.js (Mozilla) | JS | Apache 2.0 | core/evaluator.js for ToUnicode CMap handling — the cleanest readable reference for bfchar/bfrange. core/cmap.js parsing approach informed our CMap tokenizer. Their fallback chain (ToUnicode → embedded encoding → standard encoding) is what we mirror. |
| 3 | MuPDF | C | AGPL | pdf-cmap-parse.c for low-level CMap tokenizer edge cases (hex escapes inside bf entries). AGPL — read for learning only. |
| 4 | pdf-rs | Rust | MIT | pdf::content operator dispatch using a sealed enum maps cleanly to Kotlin's sealed-class pattern. Object-model layering inspired the split between parser-core (lexer/object model) and pdf-core (content semantics). |
| 5 | PdfBox-Android (Tom Roush) | Java | Apache 2.0 | Showed which PDFBox modules are safely droppable on Android (no AWT/Java2D). Validated that pure-Kotlin text extraction doesn't need any AWT-shaped APIs. |
- Operator dispatch via sealed-enum table — pdf-rs.
- Two-pass reading order: collect (x,y,text) tuples, then bucket by line band — PDFBox's
PDFTextStripperstrategy. - ToUnicode fallback chain (ToUnicode → embedded font encoding → standard-14 named encoding → raw byte) — pdf.js
core/evaluator.js. - CMap
bfrangewith array form (<0001> <0003> [<...> <...> <...>]) — both pdf.js and MuPDF handle this; we model the same. - Skip cross-reference streams gracefully — borrows the "fail soft, return empty text" stance from PdfBox-Android in handling unsupported PDF variants.
This library sits on top of dev.jaeyoung:fileloom-pdf-parser-core:0.3.0, which provides:
PdfLexer,PdfObjectParser,PdfObject(sealed hierarchy)PdfDocumentReader.open(source)→PdfLowLevelDocumentPdfLowLevelDocument.resolve(reference)— indirect-object resolutionPdfByteSource/ByteArrayPdfByteSource
We add on top:
dev.jaeyoung.fileloom.pdf.text.PdfTextExtractor— public entry point.dev.jaeyoung.fileloom.pdf.outline.PdfOutlineExtractor— outline / TOC entry point.dev.jaeyoung.fileloom.pdf.annotation.PdfAnnotationWriter— incremental Highlight and sticky-note export.dev.jaeyoung.fileloom.pdf.text.internal.*— content stream + font + CMap.
val source = ByteArrayPdfByteSource(pdfBytes)
val extractor = PdfTextExtractor.open(source)
val pageCount = extractor.pageCount
val pageText: String = extractor.extractTextForPage(pageIndex = 0)
extractor.close()PdfOutlineExtractor.open(ByteArrayPdfByteSource(pdfBytes)).use { extractor ->
val toc: List<PdfTocEntry> = extractor?.extractTableOfContents().orEmpty()
}val annotatedBytes = PdfAnnotationWriter.appendAnnotations(
pdfBytes = originalBytes,
annotations = listOf(
PdfAnnotation.Highlight(
pageIndex = 0,
rects = listOf(PdfAnnotationRect(left = 96f, top = 84f, right = 260f, bottom = 110f)),
color = PdfAnnotationColor(red = 1f, green = 0.92f, blue = 0.23f),
contents = "Important"
)
)
)- JDK 17
- Gradle 8.13+ (wrapper provided)
Run tests:
./gradlew testLocal development:
./gradlew publishToMavenLocalMaven Central bundle ZIP:
./gradlew publishToMavenCentralBundleFor non-interactive signing, pass the GPG passphrase explicitly:
SIGNING_GNUPG_PASSPHRASE='your-passphrase' ./gradlew publishToMavenCentralBundle -Pversion=0.2.3The task writes build/maven-central-bundle/fileloom-pdf-core-<version>-maven-central-bundle.zip.
It stages the jar, sources jar, javadoc jar, and POM; creates .md5 and .sha1
checksums; and GPG-signs each artifact with gpg --detach-sign --armor.
Set -Psigning.gnupg.keyName=<KEY_ID> to force a specific key, or
-Psigning.gnupg.passphrase=<PASSPHRASE> instead of the environment variable
above. Otherwise GPG's default secret key is used. This task only creates the
ZIP; upload to Maven Central is a separate manual step.
MIT. See LICENSE.