blob: 24525a9a4fa1e87ed7455b4787d66363aeee2ff9 [file] [log] [blame] [edit]
//========================================================================
//
// 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;
}