PDF注釋功能文檔
概述
本文檔詳細說明了PDF注釋功能的實現,包括注釋的加載和保存功能。該功能基于Android PDFBox庫實現,支持Ink類型注釋的讀取和寫入。
功能模塊
1. 注釋加載功能 (getAnnotation()
)
功能描述
從PDF文件中加載已存在的注釋,并將其顯示在PDFView上。
實現流程
private fun getAnnotation() {// 1. 加載PDF文檔val document = loadPdfFromAssets(this, SAMPLE_FILE) ?: return// 2. 處理加密PDFif (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 = lineGraphicsListmBinding.pdfView.redraw()}}
}
關鍵特性
- 加密PDF支持: 自動處理加密PDF的解密
- 異步加載: 使用協程避免阻塞主線程
- 線程安全: 使用
CopyOnWriteArrayList
確保線程安全 - UI更新: 加載完成后自動重繪PDF視圖
2. 注釋保存功能 (pickSave()
)
功能描述
將用戶在PDFView上繪制的注釋保存到PDF文件中,支持Ink類型注釋的寫入。
實現流程
private fun pickSave() {try {// 1. 加載PDF文檔val document = loadPdfFromAssets(this, SAMPLE_FILE) ?: returnval lineGraphicsList = mBinding.pdfView.lineGraphicsrunBlocking {// 2. 計算頁面高度映射val heightMap = HashMap<Int, Float>()val count = document.pages.countvar previousHeight = 0ffor (pageIndex in 0 until count) {val page = document.getPage(pageIndex)val curPageHeight = page.mediaBox.heightpreviousHeight += curPageHeightheightMap[pageIndex] = previousHeight}// 3. 處理每個注釋for (lineGraphic in lineGraphicsList) {if (lineGraphic.pageIndex < 0) continuewithContext(Dispatchers.IO) {// 4. 坐標轉換val inkPaths = mutableListOf<FloatArray>()val floatList = mutableListOf<Float>()val pageIndex = lineGraphic.pageIndexval page = document.getPage(pageIndex)val absolutPoints = lineGraphic.relativePoints// 5. 坐標系統轉換val pdfWidth = page.mediaBox.widthval pdfHeight = page.mediaBox.heightfor (point in absolutPoints) {val screenX = point.xval screenY = point.y// 轉換為PDF坐標系統val pdfX = screenX * pdfWidthval pdfY = (1f - screenY) * pdfHeightfloatList.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 = boundsPDPageContentStream(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 = trueinkAnnotation.isNoZoom = falseinkAnnotation.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()
)
private fun calculateInkBounds(inkPaths: MutableList<FloatArray>,pageSize: PDRectangle
): PDRectangle {var minX = Float.MAX_VALUEvar minY = Float.MAX_VALUEvar maxX = Float.MIN_VALUEvar maxY = Float.MIN_VALUEinkPaths.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()
)
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軸向下,需要反轉
轉換公式
// 屏幕坐標轉PDF坐標
val pdfX = screenX * pdfWidth
val pdfY = (1f - screenY) * pdfHeight
錯誤處理
依賴庫
核心依賴
com.tom_roush:pdfbox-android
: PDF處理核心庫com.github.barteksc:android-pdf-viewer
: PDF顯示組件org.jetbrains.kotlinx:kotlinx-coroutines
: 協程支持