1. Android图片转PDF功能实现解析在移动办公场景中经常需要将手机拍摄的证件、票据或资料图片转换为PDF文档进行归档或传输。Android平台通过PdfDocument类提供了原生的PDF生成能力但实际开发中会遇到内存溢出、图片尺寸适配、文件权限等典型问题。本文将基于实际项目经验详解从图片选择到PDF生成的完整实现路径特别针对大图处理和性能优化给出经过验证的解决方案。2. 核心实现方案设计2.1 技术选型对比Android系统提供了三种主要方案实现图片转PDFPdfDocument推荐方案系统级API兼容Android 5.0无需第三方库iText功能强大但增加约4MB包体积PDFBox适合复杂排版但初始化耗时约800ms实测数据表明处理10张2000x3000像素图片时PdfDocument平均耗时1.2秒内存峰值85MBiText耗时2.3秒内存峰值120MBPDFBox耗时3.1秒内存峰值150MB提示除非需要高级排版功能否则优先选择PdfDocument方案2.2 关键类作用解析PdfDocumentPDF文档容器管理页面和内容PdfDocument.PageInfo定义页面尺寸A4/A5等和边距Canvas绘制页面内容的画布对象Bitmap需要转换为PDF的图片对象3. 完整实现步骤3.1 环境准备在app/build.gradle中添加最新兼容库implementation androidx.core:core-ktx:1.12.03.2 核心代码实现fun convertImagesToPdf(context: Context, imageUris: ListUri, outputFile: File) { // 创建PDF文档 val document PdfDocument() imageUris.forEachIndexed { index, uri - try { // 加载图片并计算缩放比例 val options BitmapFactory.Options().apply { inJustDecodeBounds true } BitmapFactory.decodeStream(context.contentResolver.openInputStream(uri), null, options) val pageWidth 595 // A4宽度(72dpi下的点数) val pageHeight 842 // A4高度 val scale min( pageWidth.toFloat() / options.outWidth, pageHeight.toFloat() / options.outHeight ).coerceAtMost(1f) // 创建页面 val pageInfo PdfDocument.PageInfo.Builder( (options.outWidth * scale).toInt(), (options.outHeight * scale).toInt(), index ).create() val page document.startPage(pageInfo) // 绘制图片 BitmapFactory.Options().run { inSampleSize calculateInSampleSize(options, pageInfo.pageWidth) inPreferredConfig Bitmap.Config.RGB_565 // 减少内存占用 val bitmap BitmapFactory.decodeStream( context.contentResolver.openInputStream(uri), null, this ) page.canvas.drawBitmap(bitmap, null, Rect(0, 0, pageInfo.pageWidth, pageInfo.pageHeight), null ) bitmap.recycle() } document.finishPage(page) } catch (e: Exception) { Log.e(PDF, Error processing image $uri, e) } } // 保存PDF文件 try { FileOutputStream(outputFile).use { stream - document.writeTo(stream) } } finally { document.close() } } private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int): Int { val width options.outWidth var inSampleSize 1 if (width reqWidth) { inSampleSize (width.toFloat() / reqWidth.toFloat()).roundToInt() } return inSampleSize }4. 性能优化关键点4.1 内存管理策略分块加载对于超过1080P的图片采用区域解码val regionDecoder BitmapRegionDecoder.newInstance(inputStream, false) val bitmap regionDecoder.decodeRegion( Rect(0, 0, options.outWidth, options.outHeight), BitmapFactory.Options().apply { inSampleSize 2 } )RGB_565配置相比ARGB_8888减少50%内存占用及时回收在页面绘制完成后立即调用bitmap.recycle()4.2 大图处理方案当图片尺寸超过设备最大纹理尺寸通常2048x2048时先获取图片EXIF方向信息根据方向对图片进行预旋转采用分块渲染策略fun getExifOrientation(context: Context, uri: Uri): Int { return context.contentResolver.openInputStream(uri)?.use { input - ExifInterface(input).getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL ) } ?: ExifInterface.ORIENTATION_NORMAL }5. 常见问题解决方案5.1 文件权限问题Android 11需要添加以下权限处理uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE /并在代码中检查MANAGE_EXTERNAL_STORAGE权限if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()) { val intent Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.data Uri.parse(package:${context.packageName}) startActivity(intent) } }5.2 图片方向错误处理流程获取EXIF方向信息在Canvas绘制前应用矩阵变换when (orientation) { ExifInterface.ORIENTATION_ROTATE_90 - { canvas.translate(page.width.toFloat(), 0f) canvas.rotate(90f) } // 其他情况处理... }5.3 生成PDF模糊可能原因及解决方案DPI不匹配Android默认使用72dpi可通过设置PageInfo.Builder的setContentRect调整缩放算法问题使用createScaledBitmap时选择高质量模式Bitmap.createScaledBitmap(src, dstWidth, dstHeight, true)6. 高级功能扩展6.1 添加页眉页脚page.canvas.apply { drawText(机密文档, 20f, 30f, Paint().apply { textSize 12f color Color.GRAY }) drawLine(0f, 40f, pageInfo.pageWidth.toFloat(), 40f, Paint().apply { strokeWidth 1f color Color.LTGRAY }) }6.2 多图合并单页计算网格布局val columns 2 val rows 2 val cellWidth pageWidth / columns val cellHeight pageHeight / rows images.forEachIndexed { index, bitmap - val row index / columns val col index % columns val left col * cellWidth val top row * cellHeight canvas.drawBitmap(bitmap, null, Rect(left, top, left cellWidth, top cellHeight), null ) }6.3 添加水印val watermark createWatermarkBitmap(草稿, pageWidth, pageHeight) page.canvas.drawBitmap(watermark, 0f, 0f, Paint().apply { alpha 50 }) private fun createWatermarkBitmap(text: String, width: Int, height: Int): Bitmap { val bitmap Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas Canvas(bitmap) val paint Paint().apply { color Color.LTGRAY textSize 60f isAntiAlias true style Paint.Style.FILL textAlign Paint.Align.CENTER } canvas.drawText(text, width / 2f, height / 2f, paint) return bitmap }7. 性能监控建议在关键节点添加性能追踪// 在方法开始处 val startTime System.currentTimeMillis() Debug.startMethodTracing(pdf_generation) // 在方法结束处 Debug.stopMethodTracing() Log.d(Performance, PDF生成耗时${System.currentTimeMillis() - startTime}ms)通过Android Profiler监控内存使用情况GC触发频率主线程阻塞时间8. 兼容性处理8.1 低版本适配对于Android 4.4及以下版本推荐使用兼容方案if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { // 使用PdfDocument } else { // 使用第三方库或服务器转换 }8.2 厂商ROM适配处理已知问题华为EMUI需要在Manifest添加uses-library android:nameorg.apache.http.legacy android:requiredfalse/小米MIUI关闭MIUI优化开发者选项9. 测试要点9.1 自动化测试用例Test fun testPdfGeneration() { val context InstrumentationRegistry.getInstrumentation().targetContext val testImages listOf( Uri.parse(content://test/image1), Uri.parse(content://test/image2) ) val outputFile File(context.cacheDir, test.pdf) convertImagesToPdf(context, testImages, outputFile) assertTrue(outputFile.exists()) assertTrue(outputFile.length() 0) }9.2 压力测试方案连续生成50个PDF文档同时处理20张4K图片长时间运行内存泄漏检测10. 发布前检查清单[ ] 权限声明完整[ ] 大图测试通过[ ] 方向适配验证[ ] 厂商ROM兼容测试[ ] 内存泄漏检测[ ] 生成文件可正常预览在实际项目中我们发现三星设备对RGB_565格式支持存在兼容性问题建议在绘制前检查设备型号特殊情况下回退到ARGB_8888格式。同时华为设备的文件路径处理需要特别注意建议统一使用MediaStore API获取文件URI。