PDF注释的加载和保存功能的实现

PDF注释功能文档

概述

本文档详细说明了PDF注释功能的实现,包括注释的加载和保存功能。该功能基于Android PDFBox库实现,支持Ink类型注释的读取和写入。

功能模块

1. 注释加载功能 (getAnnotation())

功能描述

从PDF文件中加载已存在的注释,并将其显示在PDFView上。

实现流程
kotlin 复制代码
private fun getAnnotation() {
    // 1. 加载PDF文档
    val document = loadPdfFromAssets(this, SAMPLE_FILE) ?: return
    
    // 2. 处理加密PDF
    if (document.isEncrypted) {
        try {
            val policy = StandardProtectionPolicy("", "", AccessPermission())
            document.protect(policy)
            Log.i(TAG, "getAnnotation: --PDF解密成功")
        } catch (e: Exception) {
            Log.i(TAG, "getAnnotation: --解密失败: ${e.message}")
            document.close()
            return
        }
    }
    
    // 3. 创建线程安全的注释列表
    val lineGraphicsList = CopyOnWriteArrayList<LineGraphic>()
    
    // 4. 异步加载注释
    lifecycleScope.launch {
        val lineGraphics = PdfAnnotationLoader.loadAnnotationsFromPdf(
            context = this@MainActivity,
            document,
        )
        lineGraphicsList.addAll(lineGraphics)
        
        // 5. 更新UI显示
        if (lineGraphicsList.isNotEmpty()) {
            mBinding.pdfView.lineGraphics = lineGraphicsList
            mBinding.pdfView.redraw()
        }
    }
}

// 加载PDF文档
private fun loadPdfFromAssets(context: Context, fileName: String): PDDocument? {
    return try {
        context.assets.open(fileName).use { inputStream ->
            PDDocument.load(inputStream, MemoryUsageSetting.setupMixed(1000 * 1024 * 1024))
        }
    } catch (e: IOException) {
        e.printStackTrace()
        return null
    }
}
关键特性
  • 加密PDF支持: 自动处理加密PDF的解密
  • 异步加载: 使用协程避免阻塞主线程
  • 线程安全 : 使用CopyOnWriteArrayList确保线程安全
  • UI更新: 加载完成后自动重绘PDF视图

2. 注释保存功能 (pickSave())

功能描述

将用户在PDFView上绘制的注释保存到PDF文件中,支持Ink类型注释的写入。

实现流程
kotlin 复制代码
private fun pickSave() {
    try {
        // 1. 加载PDF文档
        val document = loadPdfFromAssets(this, SAMPLE_FILE) ?: return
        val lineGraphicsList = mBinding.pdfView.lineGraphics
        
        runBlocking {
            // 2. 计算页面高度映射
            val heightMap = HashMap<Int, Float>()
            val count = document.pages.count
            var previousHeight = 0f
            
            for (pageIndex in 0 until count) {
                val page = document.getPage(pageIndex)
                val curPageHeight = page.mediaBox.height
                previousHeight += curPageHeight
                heightMap[pageIndex] = previousHeight
            }
            
            // 3. 处理每个注释
            for (lineGraphic in lineGraphicsList) {
                if (lineGraphic.pageIndex < 0) continue
                
                withContext(Dispatchers.IO) {
                    // 4. 坐标转换
                    val inkPaths = mutableListOf<FloatArray>()
                    val floatList = mutableListOf<Float>()
                    val pageIndex = lineGraphic.pageIndex
                    val page = document.getPage(pageIndex)
                    val absolutPoints = lineGraphic.relativePoints
                    
                    // 5. 坐标系统转换
                    val pdfWidth = page.mediaBox.width
                    val pdfHeight = page.mediaBox.height
                    
                    for (point in absolutPoints) {
                        val screenX = point.x
                        val screenY = point.y
                        
                        // 转换为PDF坐标系统
                        val pdfX = screenX * pdfWidth
                        val pdfY = (1f - screenY) * pdfHeight
                        
                        floatList.add(pdfX)
                        floatList.add(pdfY)
                    }
                    inkPaths.add(floatList.toFloatArray())
                    
                    // 6. 创建Ink注释
                    val inkAnnotation = PDAnnotationInk()
                    inkAnnotation.subtype = "Ink"
                    
                    // 7. 计算边界矩形
                    val bounds = calculateInkBounds(inkPaths, page.mediaBox)
                    inkAnnotation.rectangle = bounds
                    
                    // 8. 创建外观流
                    val normalAppearance = PDAppearanceStream(document)
                    normalAppearance.bBox = bounds
                    
                    PDPageContentStream(document, normalAppearance).use { cs ->
                        cs.setStrokingColor(AWTColor.RED)
                        cs.setLineWidth(2f)
                        
                        // 绘制轨迹
                        for (path in inkPaths) {
                            cs.moveTo(path[0], path[1])
                            for (index in 2 until path.size step 2) {
                                cs.lineTo(path[index], path[index + 1])
                            }
                            cs.stroke()
                        }
                    }
                    
                    // 9. 设置外观字典
                    val apDict = COSDictionary()
                    apDict.setItem(COSName.N, normalAppearance)
                    inkAnnotation.cosObject.setItem(COSName.AP, apDict)
                    
                    // 10. 设置注释属性
                    inkAnnotation.isPrinted = true
                    inkAnnotation.isNoZoom = false
                    inkAnnotation.isNoRotate = false
                    
                    // 11. 添加到页面
                    page.annotations.add(inkAnnotation)
                }
            }
        }
        
        // 12. 保存文件
        val file = File(this.getExternalFilesDir(null), "shapes_example.pdf")
        if (file.exists()) {
            file.delete()
        }
        file.createNewFile()
        
        savePdfAsync(document, file) { result ->
            if (result.success) {
                Log.i(TAG, "保存成功")
            } else {
                Log.i(TAG, "保存失败: ${result.message}")
            }
        }
        
    } catch (e: Exception) {
        Log.i(TAG, "加载失败:${e.message}")
    }
}
关键特性
  • 坐标转换: 将屏幕坐标转换为PDF坐标系统
  • 多页面支持: 支持跨页面的注释处理
  • 异步处理: 使用协程处理IO操作
  • 外观流: 创建PDF标准的外观流确保兼容性
  • 文件保存: 异步保存到本地文件系统

辅助功能

1. 边界计算 (calculateInkBounds())

kotlin 复制代码
private fun calculateInkBounds(
    inkPaths: MutableList<FloatArray>,
    pageSize: PDRectangle
): PDRectangle {
    var minX = Float.MAX_VALUE
    var minY = Float.MAX_VALUE
    var maxX = Float.MIN_VALUE
    var maxY = Float.MIN_VALUE

    inkPaths.forEach { path ->
        for (i in path.indices step 2) {
            minX = minOf(minX, path[i])
            minY = minOf(minY, path[i + 1])
            maxX = maxOf(maxX, path[i])
            maxY = maxOf(maxY, path[i + 1])
        }
    }

    // 添加10像素边距
    return PDRectangle(
        (minX - 10).coerceAtLeast(0f),
        (minY - 10).coerceAtLeast(0f),
        (maxX - minX + 20).coerceAtMost(pageSize.width),
        (maxY - minY + 20).coerceAtMost(pageSize.height)
    )
}

2. 异步保存 (savePdfAsync())

kotlin 复制代码
private fun savePdfAsync(
    document: PDDocument,
    outputFile: File,
    callback: (SaveResult) -> Unit
) {
    CoroutineScope(Dispatchers.IO).launch {
        val result = try {
            document.save(outputFile)
            SaveResult(true, "保存成功")
        } catch (e: Exception) {
            SaveResult(false, "保存失败: ${e.message}")
        } finally {
            document.close()
        }

        withContext(Dispatchers.Main) {
            callback(result)
        }
    }
}

注释类型支持

Ink注释

  • 类型: 自由绘图注释
  • 格式: PDF标准Ink注释
  • 兼容性: 支持WPS等主流PDF阅读器
  • 属性: 颜色、线宽、边界矩形等

坐标系统

坐标转换流程

  1. 屏幕坐标: 用户在PDFView上的触摸点
  2. 相对坐标: 转换为0-1范围的相对坐标
  3. PDF坐标: 转换为PDF文档的绝对坐标
  4. Y轴反转: PDF坐标系Y轴向下,需要反转

转换公式

kotlin 复制代码
// 屏幕坐标转PDF坐标
val pdfX = screenX * pdfWidth
val pdfY = (1f - screenY) * pdfHeight

错误处理

常见错误及解决方案

  1. PDF加密: 自动尝试空密码解密
  2. 文件不存在: 检查文件路径和权限
  3. 内存不足: 使用混合内存模式
  4. 坐标转换错误: 验证坐标范围

性能优化

优化策略

  1. 异步处理: 使用协程避免阻塞UI
  2. 内存管理: 及时关闭文档释放内存
  3. 批量处理: 一次性处理多个注释
  4. 缓存机制: 缓存页面尺寸信息

依赖库

核心依赖

  • com.tom_roush:pdfbox-android: PDF处理核心库
  • com.github.barteksc:android-pdf-viewer: PDF显示组件
  • org.jetbrains.kotlinx:kotlinx-coroutines: 协程支持
相关推荐
智江鹏7 分钟前
Android 之 Kotlin 和 MVVM 架构的 Android 登录示例
android·开发语言·kotlin
凛_Lin~~34 分钟前
2025-08 安卓开发面试拷打记录(面试题)
android
网安Ruler1 小时前
Web开发-PHP应用&文件操作安全&上传下载&任意读取删除&目录遍历&文件包含
android
aningxiaoxixi2 小时前
android audio 之 Engine
android·前端·javascript
教程分享大师2 小时前
带root_兆能ZN802及兆能ZNM802融合终端安卓9系统线刷机包 当贝纯净版
android·电脑
tbit2 小时前
Flutter Provider 用法总结(更新中...)
android·flutter
whysqwhw2 小时前
Android硬件加速全景解析与深度优化指南
android
whysqwhw3 小时前
RecyclerView 快速滑动场景优化 Bitmap 加载
android
whysqwhw3 小时前
DRouter IPC简化AIDL
android