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

Saving PDF for sharing using iText with backwards compatibility

提问人:Jonas Mohr Pedersen 提问时间:2/10/2022 最后编辑:Jonas Mohr Pedersen 更新时间:2/10/2022 访问量:377

问:

在 Android 上保存 PDF 文件的问题已经困扰了我数周,使我所做的每次 Google 搜索都会导致紫色链接列表。

我尝试了多种方法,但只设法使用内置的 PDF 创建器保存 PDF 文件,这并不能达到我的目的。

我有一些问题需要回答,希望你们中的一些人能有所帮助。

问题 1:您应该在哪里保存您打算共享的 PDF 文件,然后在之后立即删除?

问题1 a)ContentResolver 和 ContentProvider 是必需的,还是仅在应用程序之间共享“目录”时?

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

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

从我所读到的内容来看,不同的 SDK 级别需要不同的文件存储方法,即:

  • <= SDK 28 使用传统的存储方式,即(使用 File API)?
  • <= SDK 29 如果使用外部存储,则应请求旧存储的权限
  • >= SDK 30 Android 强制实施分区存储

使用 SAF(存储访问框架)似乎是存储 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"
/>
Android Kotlin IO itext pdf 生成

评论

0赞 blackapps 2/10/2022
ntuitively it makes most sense for me to save the PDF in the internal cache directory, share it from there and then delete it.当然没关系。为什么要再说下去?
0赞 blackapps 2/10/2022
What is the approach for storing files with backwards compatibility?我不知道你所说的向后兼容性是什么意思。
0赞 blackapps 2/10/2022
你有两个问题。你应该发两个帖子。
0赞 mkl 2/10/2022
这似乎是一个 iText 问题,虽然 JRE 接受此正则表达式,但在 Android 上被拒绝。PatternSyntaxException
0赞 Jonas Mohr Pedersen 2/10/2022
@blackapps,因为我只需要澄清在此上下文中存储 PDfs 的最佳实践,但这不是真正的问题 - 这是我得到的 PatternSyntaxException。向后兼容性意味着,确保跨不同 SDK 级别的文件存储方法一致。

答:

1赞 mkl 2/10/2022 #1

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

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

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

在 iText 类的初始化过程中。它发生在这里: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中)中,该错误将不再发生。

评论

0赞 Jonas Mohr Pedersen 2/10/2022
哦,我从没想过它会是图书馆里的一个错误——我以为这只是我这边的愚蠢。该应用程序应该尽快投入生产,如果我不想依赖快照,您对如何处理这个问题有什么建议,直到 iText 的下一个版本?
0赞 mkl 2/10/2022
好吧,iText是开源的。因此,您可以简单地检索最新版本的源代码,并仅向后移植昨天提交的修复程序。您也可以联系 iText 支持并要求提供这样的向后移植。
0赞 Jonas Mohr Pedersen 2/10/2022
我刚刚设法重新滚动到以前的工作版本(7.1.3),所以我会坚持使用这个版本,直到下一个版本。非常感谢您的帮助,我无法计算我花了多少时间来解决这个问题。