| //======================================================================== |
| // |
| // pdfunite.cc |
| // |
| // This file is licensed under the GPLv2 or later |
| // |
| // Copyright (C) 2011-2015, 2017 Thomas Freitag <Thomas.Freitag@alfa.de> |
| // Copyright (C) 2012 Arseny Solokha <asolokha@gmx.com> |
| // Copyright (C) 2012 Fabio D'Urso <fabiodurso@hotmail.it> |
| // Copyright (C) 2012, 2014, 2017-2019, 2021, 2022 Albert Astals Cid <aacid@kde.org> |
| // Copyright (C) 2013 Adrian Johnson <ajohnson@redneon.com> |
| // Copyright (C) 2013 Hib Eris <hib@hiberis.nl> |
| // Copyright (C) 2015 Arthur Stavisky <vovodroid@gmail.com> |
| // Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, <info@kdab.com>. Work sponsored by the LiMux project of the city of Munich |
| // Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.de> |
| // Copyright (C) 2019 Marek Kasik <mkasik@redhat.com> |
| // Copyright (C) 2019, 2023 Oliver Sander <oliver.sander@tu-dresden.de> |
| // Copyright (C) 2022 crt <chluo@cse.cuhk.edu.hk> |
| // |
| //======================================================================== |
| |
| #include <PDFDoc.h> |
| #include <GlobalParams.h> |
| #include "parseargs.h" |
| #include "config.h" |
| #include <poppler-config.h> |
| #include <vector> |
| |
| static bool printVersion = false; |
| static bool printHelp = false; |
| |
| static const ArgDesc argDesc[] = { { "-v", argFlag, &printVersion, 0, "print copyright and version info" }, { "-h", argFlag, &printHelp, 0, "print usage information" }, { "-help", argFlag, &printHelp, 0, "print usage information" }, |
| { "--help", argFlag, &printHelp, 0, "print usage information" }, { "-?", argFlag, &printHelp, 0, "print usage information" }, {} }; |
| |
| static void doMergeNameTree(PDFDoc *doc, XRef *srcXRef, XRef *countRef, int oldRefNum, int newRefNum, Dict *srcNameTree, Dict *mergeNameTree, int numOffset) |
| { |
| Object mergeNameArray = mergeNameTree->lookup("Names"); |
| Object srcNameArray = srcNameTree->lookup("Names"); |
| if (mergeNameArray.isArray() && srcNameArray.isArray()) { |
| Array *newNameArray = new Array(srcXRef); |
| int j = 0; |
| for (int i = 0; i < srcNameArray.arrayGetLength() - 1; i += 2) { |
| const Object &key = srcNameArray.arrayGetNF(i); |
| const Object &value = srcNameArray.arrayGetNF(i + 1); |
| if (key.isString() && value.isRef()) { |
| while (j < mergeNameArray.arrayGetLength() - 1) { |
| const Object &mkey = mergeNameArray.arrayGetNF(j); |
| const Object &mvalue = mergeNameArray.arrayGetNF(j + 1); |
| if (mkey.isString() && mvalue.isRef()) { |
| if (mkey.getString()->cmp(key.getString()) < 0) { |
| newNameArray->add(Object(new GooString(mkey.getString()->c_str()))); |
| newNameArray->add(Object(Ref { mvalue.getRef().num + numOffset, mvalue.getRef().gen })); |
| j += 2; |
| } else if (mkey.getString()->cmp(key.getString()) == 0) { |
| j += 2; |
| } else { |
| break; |
| } |
| } else { |
| j += 2; |
| } |
| } |
| newNameArray->add(Object(new GooString(key.getString()->c_str()))); |
| newNameArray->add(Object(value.getRef())); |
| } |
| } |
| while (j < mergeNameArray.arrayGetLength() - 1) { |
| const Object &mkey = mergeNameArray.arrayGetNF(j); |
| const Object &mvalue = mergeNameArray.arrayGetNF(j + 1); |
| if (mkey.isString() && mvalue.isRef()) { |
| newNameArray->add(Object(new GooString(mkey.getString()->c_str()))); |
| newNameArray->add(Object(Ref { mvalue.getRef().num + numOffset, mvalue.getRef().gen })); |
| } |
| j += 2; |
| } |
| srcNameTree->set("Names", Object(newNameArray)); |
| doc->markPageObjects(mergeNameTree, srcXRef, countRef, numOffset, oldRefNum, newRefNum); |
| } else if (srcNameArray.isNull() && mergeNameArray.isArray()) { |
| Array *newNameArray = new Array(srcXRef); |
| for (int i = 0; i < mergeNameArray.arrayGetLength() - 1; i += 2) { |
| const Object &key = mergeNameArray.arrayGetNF(i); |
| const Object &value = mergeNameArray.arrayGetNF(i + 1); |
| if (key.isString() && value.isRef()) { |
| newNameArray->add(Object(new GooString(key.getString()->c_str()))); |
| newNameArray->add(Object(Ref { value.getRef().num + numOffset, value.getRef().gen })); |
| } |
| } |
| srcNameTree->add("Names", Object(newNameArray)); |
| doc->markPageObjects(mergeNameTree, srcXRef, countRef, numOffset, oldRefNum, newRefNum); |
| } |
| } |
| |
| static void doMergeNameDict(PDFDoc *doc, XRef *srcXRef, XRef *countRef, int oldRefNum, int newRefNum, Dict *srcNameDict, Dict *mergeNameDict, int numOffset) |
| { |
| for (int i = 0; i < mergeNameDict->getLength(); i++) { |
| const char *key = mergeNameDict->getKey(i); |
| Object mergeNameTree = mergeNameDict->lookup(key); |
| Object srcNameTree = srcNameDict->lookup(key); |
| if (srcNameTree.isDict() && mergeNameTree.isDict()) { |
| doMergeNameTree(doc, srcXRef, countRef, oldRefNum, newRefNum, srcNameTree.getDict(), mergeNameTree.getDict(), numOffset); |
| } else if (srcNameTree.isNull() && mergeNameTree.isDict()) { |
| Object newNameTree(new Dict(srcXRef)); |
| doMergeNameTree(doc, srcXRef, countRef, oldRefNum, newRefNum, newNameTree.getDict(), mergeNameTree.getDict(), numOffset); |
| srcNameDict->add(key, std::move(newNameTree)); |
| } |
| } |
| } |
| |
| static bool doMergeFormDict(Dict *srcFormDict, Dict *mergeFormDict, int numOffset) |
| { |
| Object srcFields = srcFormDict->lookup("Fields"); |
| Object mergeFields = mergeFormDict->lookup("Fields"); |
| if (srcFields.isArray() && mergeFields.isArray()) { |
| for (int i = 0; i < mergeFields.arrayGetLength(); i++) { |
| const Object &value = mergeFields.arrayGetNF(i); |
| if (!value.isRef()) { |
| error(errSyntaxError, -1, "Fields object is not a Ref."); |
| return false; |
| } |
| srcFields.arrayAdd(Object(Ref { value.getRef().num + numOffset, value.getRef().gen })); |
| } |
| } |
| return true; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////// |
| int main(int argc, char *argv[]) |
| /////////////////////////////////////////////////////////////////////////// |
| // Merge PDF files given by arguments 1 to argc-2 and write the result |
| // to the file specified by argument argc-1. |
| /////////////////////////////////////////////////////////////////////////// |
| { |
| int objectsCount = 0; |
| unsigned int numOffset = 0; |
| std::vector<Object> pages; |
| std::vector<unsigned int> offsets; |
| XRef *yRef, *countRef; |
| FILE *f; |
| OutStream *outStr; |
| int i; |
| int j, rootNum; |
| std::vector<std::unique_ptr<PDFDoc>> docs; |
| int majorVersion = 0; |
| int minorVersion = 0; |
| char *fileName = argv[argc - 1]; |
| |
| const bool ok = parseArgs(argDesc, &argc, argv); |
| if (!ok || argc < 3 || printVersion || printHelp) { |
| fprintf(stderr, "pdfunite version %s\n", PACKAGE_VERSION); |
| fprintf(stderr, "%s\n", popplerCopyright); |
| fprintf(stderr, "%s\n", xpdfCopyright); |
| if (!printVersion) { |
| printUsage("pdfunite", "<PDF-sourcefile-1>..<PDF-sourcefile-n> <PDF-destfile>", argDesc); |
| } |
| if (printVersion || printHelp) { |
| return 0; |
| } |
| return 99; |
| } |
| globalParams = std::make_unique<GlobalParams>(); |
| |
| for (i = 1; i < argc - 1; i++) { |
| std::unique_ptr<PDFDoc> doc = std::make_unique<PDFDoc>(std::make_unique<GooString>(argv[i])); |
| if (doc->isOk() && !doc->isEncrypted() && doc->getXRef()->getCatalog().isDict()) { |
| if (doc->getPDFMajorVersion() > majorVersion) { |
| majorVersion = doc->getPDFMajorVersion(); |
| minorVersion = doc->getPDFMinorVersion(); |
| } else if (doc->getPDFMajorVersion() == majorVersion) { |
| if (doc->getPDFMinorVersion() > minorVersion) { |
| minorVersion = doc->getPDFMinorVersion(); |
| } |
| } |
| docs.push_back(std::move(doc)); |
| } else if (doc->isOk()) { |
| if (doc->isEncrypted()) { |
| error(errUnimplemented, -1, "Could not merge encrypted files ('{0:s}')", argv[i]); |
| return -1; |
| } else if (!doc->getXRef()->getCatalog().isDict()) { |
| error(errSyntaxError, -1, "XRef's Catalog is not a dictionary ('{0:s}')", argv[i]); |
| return -1; |
| } |
| } else { |
| error(errSyntaxError, -1, "Could not merge damaged documents ('{0:s}')", argv[i]); |
| return -1; |
| } |
| } |
| |
| if (!(f = fopen(fileName, "wb"))) { |
| error(errIO, -1, "Could not open file '{0:s}'", fileName); |
| return -1; |
| } |
| outStr = new FileOutStream(f, 0); |
| |
| yRef = new XRef(); |
| countRef = new XRef(); |
| yRef->add(0, 65535, 0, false); |
| PDFDoc::writeHeader(outStr, majorVersion, minorVersion); |
| |
| // handle OutputIntents, AcroForm, OCProperties & Names |
| Object intents; |
| Object names; |
| Object afObj; |
| Object ocObj; |
| if (docs.size() >= 1) { |
| Object catObj = docs[0]->getXRef()->getCatalog(); |
| if (!catObj.isDict()) { |
| fclose(f); |
| delete yRef; |
| delete countRef; |
| delete outStr; |
| error(errSyntaxError, -1, "XRef's Catalog is not a dictionary."); |
| return -1; |
| } |
| Dict *catDict = catObj.getDict(); |
| intents = catDict->lookup("OutputIntents"); |
| afObj = catDict->lookupNF("AcroForm").copy(); |
| Ref *refPage = docs[0]->getCatalog()->getPageRef(1); |
| if (!afObj.isNull() && refPage) { |
| docs[0]->markAcroForm(&afObj, yRef, countRef, 0, refPage->num, refPage->num); |
| } |
| ocObj = catDict->lookupNF("OCProperties").copy(); |
| if (!ocObj.isNull() && ocObj.isDict() && refPage) { |
| docs[0]->markPageObjects(ocObj.getDict(), yRef, countRef, 0, refPage->num, refPage->num); |
| } |
| names = catDict->lookup("Names"); |
| if (!names.isNull() && names.isDict() && refPage) { |
| docs[0]->markPageObjects(names.getDict(), yRef, countRef, 0, refPage->num, refPage->num); |
| } |
| if (intents.isArray() && intents.arrayGetLength() > 0) { |
| for (i = 1; i < (int)docs.size(); i++) { |
| Object pagecatObj = docs[i]->getXRef()->getCatalog(); |
| Dict *pagecatDict = pagecatObj.getDict(); |
| Object pageintents = pagecatDict->lookup("OutputIntents"); |
| if (pageintents.isArray() && pageintents.arrayGetLength() > 0) { |
| for (j = intents.arrayGetLength() - 1; j >= 0; j--) { |
| Object intent = intents.arrayGet(j, 0); |
| if (intent.isDict()) { |
| Object idf = intent.dictLookup("OutputConditionIdentifier"); |
| if (idf.isString()) { |
| const GooString *gidf = idf.getString(); |
| bool removeIntent = true; |
| for (int k = 0; k < pageintents.arrayGetLength(); k++) { |
| Object pgintent = pageintents.arrayGet(k, 0); |
| if (pgintent.isDict()) { |
| Object pgidf = pgintent.dictLookup("OutputConditionIdentifier"); |
| if (pgidf.isString()) { |
| const GooString *gpgidf = pgidf.getString(); |
| if (gpgidf->cmp(gidf) == 0) { |
| removeIntent = false; |
| break; |
| } |
| } |
| } |
| } |
| if (removeIntent) { |
| intents.arrayRemove(j); |
| error(errSyntaxWarning, -1, "Output intent {0:s} missing in pdf {1:s}, removed", gidf->c_str(), docs[i]->getFileName()->c_str()); |
| } |
| } else { |
| intents.arrayRemove(j); |
| error(errSyntaxWarning, -1, "Invalid output intent dict, missing required OutputConditionIdentifier"); |
| } |
| } else { |
| intents.arrayRemove(j); |
| } |
| } |
| } else { |
| error(errSyntaxWarning, -1, "Output intents differs, remove them all"); |
| break; |
| } |
| } |
| } |
| if (intents.isArray() && intents.arrayGetLength() > 0) { |
| for (j = intents.arrayGetLength() - 1; j >= 0; j--) { |
| Object intent = intents.arrayGet(j, 0); |
| if (intent.isDict()) { |
| docs[0]->markPageObjects(intent.getDict(), yRef, countRef, numOffset, 0, 0); |
| } else { |
| intents.arrayRemove(j); |
| } |
| } |
| } |
| } |
| |
| for (i = 0; i < (int)docs.size(); i++) { |
| for (j = 1; j <= docs[i]->getNumPages(); j++) { |
| if (!docs[i]->getCatalog()->getPage(j)) { |
| continue; |
| } |
| |
| const PDFRectangle *cropBox = nullptr; |
| if (docs[i]->getCatalog()->getPage(j)->isCropped()) { |
| cropBox = docs[i]->getCatalog()->getPage(j)->getCropBox(); |
| } |
| if (!docs[i]->replacePageDict(j, docs[i]->getCatalog()->getPage(j)->getRotate(), docs[i]->getCatalog()->getPage(j)->getMediaBox(), cropBox)) { |
| fclose(f); |
| delete yRef; |
| delete countRef; |
| delete outStr; |
| error(errSyntaxError, -1, "PDFDoc::replacePageDict failed."); |
| return -1; |
| } |
| Ref *refPage = docs[i]->getCatalog()->getPageRef(j); |
| Object page = docs[i]->getXRef()->fetch(*refPage); |
| Dict *pageDict = page.getDict(); |
| Object *resDict = docs[i]->getCatalog()->getPage(j)->getResourceDictObject(); |
| if (resDict->isDict()) { |
| pageDict->set("Resources", resDict->copy()); |
| } |
| pages.push_back(std::move(page)); |
| offsets.push_back(numOffset); |
| docs[i]->markPageObjects(pageDict, yRef, countRef, numOffset, refPage->num, refPage->num); |
| Object annotsObj = pageDict->lookupNF("Annots").copy(); |
| if (!annotsObj.isNull()) { |
| docs[i]->markAnnotations(&annotsObj, yRef, countRef, numOffset, refPage->num, refPage->num); |
| } |
| } |
| Object pageCatObj = docs[i]->getXRef()->getCatalog(); |
| if (!pageCatObj.isDict()) { |
| fclose(f); |
| delete yRef; |
| delete countRef; |
| delete outStr; |
| error(errSyntaxError, -1, "XRef's Catalog is not a dictionary."); |
| return -1; |
| } |
| Dict *pageCatDict = pageCatObj.getDict(); |
| Object pageNames = pageCatDict->lookup("Names"); |
| if (!pageNames.isNull() && pageNames.isDict()) { |
| if (!names.isDict()) { |
| names = Object(new Dict(yRef)); |
| } |
| doMergeNameDict(docs[i].get(), yRef, countRef, 0, 0, names.getDict(), pageNames.getDict(), numOffset); |
| } |
| Object pageForm = pageCatDict->lookup("AcroForm"); |
| if (i > 0 && !pageForm.isNull() && pageForm.isDict()) { |
| if (afObj.isNull()) { |
| afObj = pageCatDict->lookupNF("AcroForm").copy(); |
| } else if (afObj.isDict()) { |
| if (!doMergeFormDict(afObj.getDict(), pageForm.getDict(), numOffset)) { |
| fclose(f); |
| delete yRef; |
| delete countRef; |
| delete outStr; |
| return -1; |
| } |
| } |
| } |
| objectsCount += docs[i]->writePageObjects(outStr, yRef, numOffset, true); |
| numOffset = yRef->getNumObjects() + 1; |
| } |
| |
| rootNum = yRef->getNumObjects() + 1; |
| yRef->add(rootNum, 0, outStr->getPos(), true); |
| outStr->printf("%d 0 obj\n", rootNum); |
| outStr->printf("<< /Type /Catalog /Pages %d 0 R", rootNum + 1); |
| // insert OutputIntents |
| if (intents.isArray() && intents.arrayGetLength() > 0) { |
| outStr->printf(" /OutputIntents ["); |
| for (j = 0; j < intents.arrayGetLength(); j++) { |
| Object intent = intents.arrayGet(j, 0); |
| if (intent.isDict()) { |
| PDFDoc::writeObject(&intent, outStr, yRef, 0, nullptr, cryptRC4, 0, 0, 0); |
| } |
| } |
| outStr->printf("]"); |
| } |
| // insert AcroForm |
| if (!afObj.isNull()) { |
| outStr->printf(" /AcroForm "); |
| PDFDoc::writeObject(&afObj, outStr, yRef, 0, nullptr, cryptRC4, 0, 0, 0); |
| } |
| // insert OCProperties |
| if (!ocObj.isNull() && ocObj.isDict()) { |
| outStr->printf(" /OCProperties "); |
| PDFDoc::writeObject(&ocObj, outStr, yRef, 0, nullptr, cryptRC4, 0, 0, 0); |
| } |
| // insert Names |
| if (!names.isNull() && names.isDict()) { |
| outStr->printf(" /Names "); |
| PDFDoc::writeObject(&names, outStr, yRef, 0, nullptr, cryptRC4, 0, 0, 0); |
| } |
| outStr->printf(">>\nendobj\n"); |
| objectsCount++; |
| |
| yRef->add(rootNum + 1, 0, outStr->getPos(), true); |
| outStr->printf("%d 0 obj\n", rootNum + 1); |
| outStr->printf("<< /Type /Pages /Kids ["); |
| for (j = 0; j < (int)pages.size(); j++) { |
| outStr->printf(" %d 0 R", rootNum + j + 2); |
| } |
| outStr->printf(" ] /Count %zd >>\nendobj\n", pages.size()); |
| objectsCount++; |
| |
| for (i = 0; i < (int)pages.size(); i++) { |
| yRef->add(rootNum + i + 2, 0, outStr->getPos(), true); |
| outStr->printf("%d 0 obj\n", rootNum + i + 2); |
| outStr->printf("<< "); |
| Dict *pageDict = pages[i].getDict(); |
| for (j = 0; j < pageDict->getLength(); j++) { |
| if (j > 0) { |
| outStr->printf(" "); |
| } |
| const char *key = pageDict->getKey(j); |
| Object value = pageDict->getValNF(j).copy(); |
| if (strcmp(key, "Parent") == 0) { |
| outStr->printf("/Parent %d 0 R", rootNum + 1); |
| } else { |
| outStr->printf("/%s ", key); |
| PDFDoc::writeObject(&value, outStr, yRef, offsets[i], nullptr, cryptRC4, 0, 0, 0); |
| } |
| } |
| outStr->printf(" >>\nendobj\n"); |
| objectsCount++; |
| } |
| Goffset uxrefOffset = outStr->getPos(); |
| Ref ref; |
| ref.num = rootNum; |
| ref.gen = 0; |
| Object trailerDict = PDFDoc::createTrailerDict(objectsCount, false, 0, &ref, yRef, fileName, outStr->getPos()); |
| PDFDoc::writeXRefTableTrailer(std::move(trailerDict), yRef, true, // write all entries according to ISO 32000-1, 7.5.4 Cross-Reference Table: "For a file that has never been incrementally updated, the cross-reference section shall |
| // contain only one subsection, whose object numbering begins at 0." |
| uxrefOffset, outStr, yRef); |
| |
| outStr->close(); |
| delete outStr; |
| fclose(f); |
| delete yRef; |
| delete countRef; |
| return 0; |
| } |