在iOS上合并PDF文件

12

iOS中是否有一种方法可以合并PDF文件,即将一个PDF文件的页面附加在另一个PDF文件的末尾并将其保存到磁盘上?


我相信FastPdfKit正是你所需要的! - Icemanind
FastPdfKit是一个很好的用于显示PDF的库,但据我所知它不支持合并。 - lkraider
这并不是针对此问题的答案,但如果有人想要追加一个现有的PDF文件 - Hemang
7个回答

23

我对Jonathan的代码进行了一些小重构,以合并任何大小的PDF文件:

+ (NSString *)joinPDF:(NSArray *)listOfPaths {
    // File paths
    NSString *fileName = @"ALL.pdf";
    NSString *pdfPathOutput = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:fileName];

    CFURLRef pdfURLOutput = (  CFURLRef)CFBridgingRetain([NSURL fileURLWithPath:pdfPathOutput]);

    NSInteger numberOfPages = 0;
    // Create the output context
    CGContextRef writeContext = CGPDFContextCreateWithURL(pdfURLOutput, NULL, NULL);

    for (NSString *source in listOfPaths) {
        CFURLRef pdfURL = (  CFURLRef)CFBridgingRetain([[NSURL alloc] initFileURLWithPath:source]);

        //file ref
        CGPDFDocumentRef pdfRef = CGPDFDocumentCreateWithURL((CFURLRef) pdfURL);
        numberOfPages = CGPDFDocumentGetNumberOfPages(pdfRef);

        // Loop variables
        CGPDFPageRef page;
        CGRect mediaBox;

        // Read the first PDF and generate the output pages
        DLog(@"GENERATING PAGES FROM PDF 1 (%@)...", source);
        for (int i=1; i<=numberOfPages; i++) {
            page = CGPDFDocumentGetPage(pdfRef, i);
            mediaBox = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
            CGContextBeginPage(writeContext, &mediaBox);
            CGContextDrawPDFPage(writeContext, page);
            CGContextEndPage(writeContext);
        }

        CGPDFDocumentRelease(pdfRef);
        CFRelease(pdfURL);
    }
    CFRelease(pdfURLOutput);

    // Finalize the output file
    CGPDFContextClose(writeContext);
    CGContextRelease(writeContext);

    return pdfPathOutput;
}
希望这有所帮助。

2
仅针对ARC的更新:似乎除非您使用自动释放方法fileURLWithPath: --> (__bridge CFURLRef)[NSURL fileURLWithPath:pdfPathOutput],否则这将抛出异常,而不是使用init方法。 - Bek
1
原始的PDF在尺寸和兼容版本方面有点不同,只考虑合并一个PDF文件,原始版本为1.4,但生成的合并版为1.3。我该如何设置版本? - Cam

21

我想出了这个解决方案:

// Documents dir
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];

// File paths
NSString *pdfPath1 = [documentsDirectory stringByAppendingPathComponent:@"1.pdf"];
NSString *pdfPath2 = [documentsDirectory stringByAppendingPathComponent:@"2.pdf"];
NSString *pdfPathOutput = [documentsDirectory stringByAppendingPathComponent:@"out.pdf"];

// File URLs
CFURLRef pdfURL1 = (CFURLRef)[[NSURL alloc] initFileURLWithPath:pdfPath1];
CFURLRef pdfURL2 = (CFURLRef)[[NSURL alloc] initFileURLWithPath:pdfPath2];
CFURLRef pdfURLOutput = (CFURLRef)[[NSURL alloc] initFileURLWithPath:pdfPathOutput];

// File references
CGPDFDocumentRef pdfRef1 = CGPDFDocumentCreateWithURL((CFURLRef) pdfURL1);
CGPDFDocumentRef pdfRef2 = CGPDFDocumentCreateWithURL((CFURLRef) pdfURL2);

// Number of pages
NSInteger numberOfPages1 = CGPDFDocumentGetNumberOfPages(pdfRef1);
NSInteger numberOfPages2 = CGPDFDocumentGetNumberOfPages(pdfRef2);

// Create the output context
CGContextRef writeContext = CGPDFContextCreateWithURL(pdfURLOutput, NULL, NULL);

// Loop variables
CGPDFPageRef page;
CGRect mediaBox;

// Read the first PDF and generate the output pages
NSLog(@"GENERATING PAGES FROM PDF 1 (%i)...", numberOfPages1);
for (int i=1; i<=numberOfPages1; i++) {
    page = CGPDFDocumentGetPage(pdfRef1, i);
    mediaBox = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
    CGContextBeginPage(writeContext, &mediaBox);
    CGContextDrawPDFPage(writeContext, page);
    CGContextEndPage(writeContext);
}

// Read the second PDF and generate the output pages
NSLog(@"GENERATING PAGES FROM PDF 2 (%i)...", numberOfPages2);
for (int i=1; i<=numberOfPages2; i++) {
    page = CGPDFDocumentGetPage(pdfRef2, i);
    mediaBox = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
    CGContextBeginPage(writeContext, &mediaBox);
    CGContextDrawPDFPage(writeContext, page);
    CGContextEndPage(writeContext);      
}
NSLog(@"DONE!");

// Finalize the output file
CGPDFContextClose(writeContext);

// Release from memory
CFRelease(pdfURL1);
CFRelease(pdfURL2);
CFRelease(pdfURLOutput);
CGPDFDocumentRelease(pdfRef1);
CGPDFDocumentRelease(pdfRef2);
CGContextRelease(writeContext);

这里最大的问题是内存分配。如你所见,在这种方法中,你必须同时读取要合并的两个PDF文件,并生成输出。释放内存只在最后发生。我尝试将一个包含500页(约15MB)的PDF文件与另一个包含100页(约3MB)的文件合并,它产生了一个新的拥有600页(当然!)仅大小约为5MB(神奇?)。执行时间约为30秒(考虑到iPad 1还好),分配了17MB的内存(痛苦!)。应用程序幸运地没有崩溃,但我认为iOS会乐意杀死像这样消耗17MB内存的应用程序。;P


修改代码以便在每个文档(甚至每x页)写入磁盘时释放它们不应该很难。事实上,你甚至可以一次读取和释放一页!这是速度和内存之间的权衡。 - FeifanZ
那个方法可行!虽然我还没有找到实现它的方法。使用CGPDFContextClose关闭上下文(将文件保存到磁盘)后,无法重新打开并继续编辑,就像将指针移动到文件末尾并添加新内容一样。 - Jonatan
嗯...我对下面的函数和方法不太熟悉,但可能有一些文件I/O流类可以让你推进指针。或者每次提取大约一兆比特并将其写入磁盘(不确定这样做的效果如何)。 - FeifanZ
这对我来说非常完美,我每次只合并4-5页。 - PruitIgoe
非常感谢您的出色回答!我一直在寻找一个好的例子,可以将几个PDF文件中不同大小的页面合并在一起,而这个例子是最好的!但有一个注意点,就是页面尺寸的确定方式。您使用了 CGPDFPageGetBoxRect(page, kCGPDFMediaBox),但参数 kCGPDFMediaBox 可能并不总是最佳选择。在一个用于印刷杂志的文件中,前后封面印在同一张纸上,使用 kCGPDFMediaBox 会使我在同一页上得到两个封面,而 kCGPDFCropBox 则可以正确地裁剪可见页面。 - SaltyNuts

6

我的Swift 3函数:

// sourcePdfFiles is array of source file full paths, destPdfFile is dest file full path
func mergePdfFiles(sourcePdfFiles:[String], destPdfFile:String) {

    guard UIGraphicsBeginPDFContextToFile(destPdfFile, CGRect.zero, nil) else {
        return
    }
    guard let destContext = UIGraphicsGetCurrentContext() else {
        return
    }

    for index in 0 ..< sourcePdfFiles.count {
        let pdfFile = sourcePdfFiles[index]
        let pdfUrl = NSURL(fileURLWithPath: pdfFile)
        guard let pdfRef = CGPDFDocument(pdfUrl) else {
            continue
        }

        for i in 1 ... pdfRef.numberOfPages {
            if let page = pdfRef.page(at: i) {
                var mediaBox = page.getBoxRect(.mediaBox)
                destContext.beginPage(mediaBox: &mediaBox)
                destContext.drawPDFPage(page)
                destContext.endPage()
            }
        }
    }

    destContext.closePDF()
    UIGraphicsEndPDFContext()
}

这非常有帮助。谢谢! :) - Clifton Labrum
在这里,你能帮我获取最终保存在磁盘上的PDF文件吗? - Jincy Varughese
@JincyVarughese,它已保存到磁盘作为destPdfFile。 - Sam Xu

3

我想分享一下使用Swift的答案,因为我在查找Swift时没有找到它,不得不进行翻译。此外,我的答案使用了一个名为pdfPagesURLArray的单独pdf数组,并循环生成完整的pdf。我还是个新手,欢迎任何建议。

    let file = "fileName.pdf"
    guard var documentPaths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first else {
        NSLog("Doh - can't find that path")
        return
    }
    documentPaths = documentPaths.stringByAppendingString(file)
    print(documentPaths)

    let fullPDFOutput: CFURLRef = NSURL(fileURLWithPath: documentPaths)

    let writeContext = CGPDFContextCreateWithURL(fullPDFOutput, nil, nil)

    for pdfURL in pdfPagesURLArray {
        let pdfPath: CFURLRef = NSURL(fileURLWithPath: pdfURL)
        let pdfReference = CGPDFDocumentCreateWithURL(pdfPath)
        let numberOfPages = CGPDFDocumentGetNumberOfPages(pdfReference)
        var page: CGPDFPageRef
        var mediaBox: CGRect

        for index in 1...numberOfPages {

你可以像这样强制解包:

page = CGPDFDocumentGetPage(pdfReference, index)!

但是为了遵循最佳实践,建议采用以下方式:

        guard let getCGPDFPage = CGPDFDocumentGetPage(pdfReference, index) else {
                NSLog("Error occurred in creating page")
                return
            }
            page = getCGPDFPage
            mediaBox = CGPDFPageGetBoxRect(page, .MediaBox)
            CGContextBeginPage(writeContext, &mediaBox)
            CGContextDrawPDFPage(writeContext, page)
            CGContextEndPage(writeContext)
        }
    }
    NSLog("DONE!")

    CGPDFContextClose(writeContext);

    NSLog(documentPaths)

非常感谢,我正在从HTML创建PDF并需要分页。由于我找不到任何方法来实现它,所以我创建了多个带有多个HTML的PDF,然后使用了您的代码将它们合并在一起,非常感谢 :) - vinbhai4u
1
@vinbhai4u 我在Swift中找不到任何解决方案,所以我希望我想出的解决方法对其他人有所帮助。很高兴它能帮到你 :) - FromTheStix

3

Swift 5:

import PDFKit

合并PDF文件,保留链接等信息,操作步骤如下:
func mergePdf(data: Data, otherPdfDocumentData: Data) -> PDFDocument {
    // get the pdfData
    let pdfDocument = PDFDocument(data: data)!
    let otherPdfDocument = PDFDocument(data: otherPdfDocumentData)!
    
    // create new PDFDocument
    let newPdfDocument = PDFDocument()

    // insert all pages of first document
    for p in 0..<pdfDocument.pageCount {
        let page = pdfDocument.page(at: p)!
        let copiedPage = page.copy() as! PDFPage // from docs
        newPdfDocument.insert(copiedPage, at: newPdfDocument.pageCount)
    }

    // insert all pages of other document
    for q in 0..<otherPdfDocument.pageCount {
        let page = pdfDocument.page(at: q)!
        let copiedPage = page.copy() as! PDFPage
        newPdfDocument.insert(copiedPage, at: newPdfDocument.pageCount)
    }
    return newPdfDocument
}

PDF文档的插入功能可以在文档中找到,文档中写道:

open class PDFDocument : NSObject, NSCopying {
...
// Methods allowing pages to be inserted, removed, and re-ordered. Can throw range exceptions.
// Note: when inserting a PDFPage, you have to be careful if that page came from another PDFDocument. PDFPage's have a 
// notion of a single document that owns them and when you call the methods below the PDFPage passed in is assigned a 
// new owning document.  You'll want to call -[PDFPage copy] first then and pass this copy to the blow methods. This 
// allows the orignal PDFPage to maintain its original document.
open func insert(_ page: PDFPage, at index: Int)

open func removePage(at index: Int)

open func exchangePage(at indexA: Int, withPageAt indexB: Int)
...
}

在您的类中创建 mergedPdf 变量:

    var mergedPdf: PDFDocument?

viewDidLoad()调用合并函数并显示合并后的PDF文件:
    mergedPdf = mergePdf(data: pdfData1, otherPdfDocumentData: pdfData2)
    // show merged pdf in pdfView
    PDFView.document = mergedPdf!
    

保存您的PDF文件

首先按照以下步骤将新PDF文件转换:

let documentDataForSaving = mergedPdf.dataRepresentation()

documentDataForSaving放入以下函数中: 使用保存函数:

let urlWhereTheFileIsSaved = writeDataToTemporaryDirectory(withFilename: "My File Name", inFolder: nil, data: documentDataForSaving)

也许您希望在文件名中避免使用/,并且不要使其超过256个字符的长度。

保存功能:

// Export PDF to directory, e.g. here for sharing
    func writeDataToTemporaryDirectory(withFilename: String, inFolder: String?, data: Data) -> URL? {
        do {
            // get a directory
            var temporaryDirectory = FileManager.default.temporaryDirectory // for e.g. sharing
            // FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! // to make it public in user's directory (update plist for user access) 
            // FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).last! // to hide it from any user interactions) 
            // do you want to create subfolder?
            if let inFolder = inFolder {
                temporaryDirectory = temporaryDirectory.appendingPathComponent(inFolder)
                if !FileManager.default.fileExists(atPath: temporaryDirectory.absoluteString) {
                    do {
                        try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
                    } catch {
                        print(error.localizedDescription);
                    }
                }
            }
            // name the file
            let temporaryFileURL = temporaryDirectory.appendingPathComponent(withFilename)
            print("writeDataToTemporaryDirectory at url:\t\(temporaryFileURL)")
            try data.write(to: temporaryFileURL)
            return temporaryFileURL
        } catch {
            print(error)
        }
        return nil
    }

我正在尝试让它工作,但似乎有些变化了。在PDFDocument上的insert方法期望一个类型为PDFPage的对象,而不是另一个PDFDocument。你是否碰巧在Xcode 13.3中尝试过这个? - Clifton Labrum
谢谢你提醒我!实际上,我已经有一段时间没有打开我的项目了。我会尽快检查并更新。 - FrugalResolution
@CliftonLabrum 我已经添加了一个复制函数来考虑文档,但是确实,insertPDFDocument的一个函数,需要一个PDFPage,我复制并粘贴了我的代码,现在它可以完美运行了,你还有什么问题吗? - FrugalResolution
1
不用了,我搞定了。谢谢! - Clifton Labrum

1
我基于@matsoftware创建的解决方案创建了我的解决方案。
我为我的解决方案创建了一个代码片段:https://gist.github.com/jefferythomas/7265536
+ (void)combinePDFURLs:(NSArray *)PDFURLs writeToURL:(NSURL *)URL
{
    CGContextRef context = CGPDFContextCreateWithURL((__bridge CFURLRef)URL, NULL, NULL);

    for (NSURL *PDFURL in PDFURLs) {
        CGPDFDocumentRef document = CGPDFDocumentCreateWithURL((__bridge CFURLRef)PDFURL);
        size_t numberOfPages = CGPDFDocumentGetNumberOfPages(document);

        for (size_t pageNumber = 1; pageNumber <= numberOfPages; ++pageNumber) {
            CGPDFPageRef page = CGPDFDocumentGetPage(document, pageNumber);
            CGRect mediaBox = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);

            CGContextBeginPage(context, &mediaBox);
            CGContextDrawPDFPage(context, page);
            CGContextEndPage(context);
        }

        CGPDFDocumentRelease(document);
    }

    CGPDFContextClose(context);
    CGContextRelease(context);
}

1
我在这里推广自己的库......但是我有一个免费的PDF阅读/写入库,最近展示了如何在iOS环境中使用它。它非常适合合并和操作PDF,并且可以在相对较小的内存占用下进行操作。考虑使用它,这里是一个例子- ios with PDFHummus。再次强调,这是我在宣传自己的库,因此请在正确的上下文中接受这个建议。

似乎这个已经不存在了。 - Christian Di Lorenzo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接