使用具有向后兼容性的 iText 保存 PDF 以供共享

Saving PDF for sharing using iText with backwards compatibility

在 Android 上保存 PDF 文件的问题困扰了我几个星期,每次 Google 搜索我都会得到一个紫色链接列表。

我尝试了很多方法,但只能使用内置的 PDF 创建器保存 PDF 文件,这不能满足我的目的。

我有很多问题想寻求答案,希望你们中的一些人能提供帮助。

问题 1:要分享然后立即删除的 PDF 文件应该保存在哪里?

Q1 a) 是否需要 ContentResolver 和 ContentProvider,或者这仅在应用程序之间共享 'directory' 时才需要?

直觉上,将 PDF 保存在内部缓存目录中、从那里共享然后删除它对我来说最有意义。

问题2:向后兼容的文件存储方式是什么?

据我了解,不同的 SDK 级别需要不同的文件存储方法,即:

使用 SAF(存储访问框架)似乎是 recommended way 存储 PDF 等文档的方法。但是,当用户只是生成和共享 PDF 时,我不打算向他们显示文件选择器。

我遇到了这个非常普遍的异常,它发生在 iText 关闭输出流时。下面是一些代码示例及其产生的错误。

SDK 30 文件API测试

fun createPdfInternalSDK30(context: Context) {
    val fileName = "test.pdf"

    //  MODE_PRIVATE creates in internal storage, right?
    val out = context.openFileOutput(fileName, Context.MODE_PRIVATE)
    // out path: /data/user/0/dk.overlevelsesguiden.de10her/files/test.pdf

    try {
        val writer = PdfWriter(out)
        val pdf = PdfDocument(writer)
        
        Document(pdf, PageSize.A4, false).apply { 
            add(Paragraph("Test"))
            close()
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

错误

java.lang.ExceptionInInitializerError
        at com.itextpdf.commons.actions.producer.ProducerBuilder.modifyProducer(ProducerBuilder.java:97)
        at com.itextpdf.kernel.actions.events.FlushPdfDocumentEvent.doAction(FlushPdfDocumentEvent.java:103)
        at com.itextpdf.commons.actions.EventManager.onEvent(EventManager.java:71)
        at com.itextpdf.kernel.pdf.PdfDocument.close(PdfDocument.java:849)
        at com.itextpdf.layout.Document.close(Document.java:117)
        at dk.overlevelsesguiden.de10her.business.PDFService.createPdfInternalSDK30(PDFService.kt:118)
    Caused by: java.util.regex.PatternSyntaxException: Syntax error in regexp pattern near index 12
        $\{([^}]*)}

SDK 29+ 分区存储测试

    @RequiresApi(api = Build.VERSION_CODES.Q)
    fun createPdfSharedSDK29plus(context: Context) {
        val pdfCollection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, "title")
            put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
        }

        var uri: Uri? = null

        try {
            uri = context.contentResolver.insert(pdfCollection, contentValues) ?: throw IOException("Failed to create new MediaStore record")
            val out = context.contentResolver.openOutputStream(uri)
            val writer = PdfWriter(out)
            val pdf = PdfDocument(writer)
            Document(pdf, PageSize.A4, false).apply {
                add(Paragraph("Test"))
                close()
            }
        } catch (e: IOException) {
            uri?.let { orphanUri ->
                context.contentResolver.delete(orphanUri, null, null)
            }
        }
    }

错误

java.lang.ExceptionInInitializerError
        at com.itextpdf.commons.actions.producer.ProducerBuilder.modifyProducer(ProducerBuilder.java:97)
        at com.itextpdf.kernel.actions.events.FlushPdfDocumentEvent.doAction(FlushPdfDocumentEvent.java:103)
        at com.itextpdf.commons.actions.EventManager.onEvent(EventManager.java:71)
        at com.itextpdf.kernel.pdf.PdfDocument.close(PdfDocument.java:849)
        at com.itextpdf.layout.Document.close(Document.java:117)
        at dk.overlevelsesguiden.de10her.business.PDFService.finalTest(PDFService.kt:118)
    Caused by: java.util.regex.PatternSyntaxException: Syntax error in regexp pattern near index 12
        $\{([^}]*)}

清单权限

<!-- Without this folders will be inaccessible in Android-11 and above devices -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    
<!-- Without this entry storage-permission entry will not be visible under app-info permissions list Android-10 and below -->
<uses-permission 
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28"
/>

您提出了很多问题。此答案侧重于您显示的实际错误。

您显示的实际错误都会导致

java.util.regex.PatternSyntaxException: Syntax error in regexp pattern near index 12
        $\{([^}]*)}

在 iText 初始化期间 class ProducerBuilder。它发生在这里:

private static final String PATTERN_STRING = "\$\{([^}]*)}";
private static final Pattern PATTERN = Pattern.compile(PATTERN_STRING);

实际上,JRE 中的正则表达式与 Android 中的不同。在普通的 Java 中,正则表达式 $\{([^}]*)} 被接受,一个 } 表示如果没有未转义的开头 而不是 则字符需要转义 {之前。 Android,另一方面,要求它即使在那时也被转义。

昨天已提交对此的修复 - https://github.com/itext/itext7/commit/32bf2552770866dce4516798e7b11f26631ae92f - 现在的模式字符串是:

private static final String PATTERN_STRING = "\$\{([^}]*)\}";

因此,在下一个 iText 版本中(或在真正当前的 SNAPSHOT 中),不会再出现这个错误。