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阅读器
- 属性: 颜色、线宽、边界矩形等
坐标系统
坐标转换流程
- 屏幕坐标: 用户在PDFView上的触摸点
- 相对坐标: 转换为0-1范围的相对坐标
- PDF坐标: 转换为PDF文档的绝对坐标
- Y轴反转: PDF坐标系Y轴向下,需要反转
转换公式
kotlin
// 屏幕坐标转PDF坐标
val pdfX = screenX * pdfWidth
val pdfY = (1f - screenY) * pdfHeight
错误处理
常见错误及解决方案
- PDF加密: 自动尝试空密码解密
- 文件不存在: 检查文件路径和权限
- 内存不足: 使用混合内存模式
- 坐标转换错误: 验证坐标范围
性能优化
优化策略
- 异步处理: 使用协程避免阻塞UI
- 内存管理: 及时关闭文档释放内存
- 批量处理: 一次性处理多个注释
- 缓存机制: 缓存页面尺寸信息
依赖库
核心依赖
com.tom_roush:pdfbox-android
: PDF处理核心库com.github.barteksc:android-pdf-viewer
: PDF显示组件org.jetbrains.kotlinx:kotlinx-coroutines
: 协程支持