Код в этом ответе является попыткой оптимизировать документы, подобные примеру документа ОП, то есть документы, содержащие копии абсолютно идентичных объектов, в данном случае - полностью идентичные, полностью встроенные шрифты.Он не объединяет просто почти идентичные объекты, например, несколько подмножеств одного и того же шрифта в одно единственное подмножество объединения.
В ходе комментариев к вопросам выяснилось, что дубликаты шрифтов в PDF-файле OPдействительно были идентичные полные копии файла исходного шрифта.Чтобы объединить такие дубликаты объектов, необходимо собрать сложные объекты (массивы, словари, потоки) документа, сравнить их друг с другом, а затем объединить дубликаты.
В качестве фактического попарного сравнения всех сложных объектовдокумент может занимать слишком много времени в случае больших документов, следующий код вычисляет хэш этих объектов и сравнивает объекты только с одинаковым хешем.
Для объединения дубликатов код выбирает один из дубликатов и заменяет всессылки на любые другие дубликаты со ссылкой на выбранный, удаляя другие дубликаты из пула объектов документа.Чтобы сделать это более эффективно, код изначально собирает не только все сложные объекты, но и все ссылки на каждый из них.
Код оптимизации
Этот метод вызывается для оптимизации * 1013.*:
public void optimize(PDDocument pdDocument) throws IOException {
Map<COSBase, Collection<Reference>> complexObjects = findComplexObjects(pdDocument);
for (int pass = 0; ; pass++) {
int merges = mergeDuplicates(complexObjects);
if (merges <= 0) {
System.out.printf("Pass %d - No merged objects\n\n", pass);
break;
}
System.out.printf("Pass %d - Merged objects: %d\n\n", pass, merges);
}
}
( OptimizeAfterMerge тестируемый метод)
Оптимизация выполняется за несколько проходов, поскольку равенство некоторых объектов можно распознать толькопосле того, как дубликаты, на которые они ссылаются, были объединены.
Следующие вспомогательные методы и классы собирают сложные объекты PDF и ссылки на каждый из них:
Map<COSBase, Collection<Reference>> findComplexObjects(PDDocument pdDocument) {
COSDictionary catalogDictionary = pdDocument.getDocumentCatalog().getCOSObject();
Map<COSBase, Collection<Reference>> incomingReferences = new HashMap<>();
incomingReferences.put(catalogDictionary, new ArrayList<>());
Set<COSBase> lastPass = Collections.<COSBase>singleton(catalogDictionary);
Set<COSBase> thisPass = new HashSet<>();
while(!lastPass.isEmpty()) {
for (COSBase object : lastPass) {
if (object instanceof COSArray) {
COSArray array = (COSArray) object;
for (int i = 0; i < array.size(); i++) {
addTarget(new ArrayReference(array, i), incomingReferences, thisPass);
}
} else if (object instanceof COSDictionary) {
COSDictionary dictionary = (COSDictionary) object;
for (COSName key : dictionary.keySet()) {
addTarget(new DictionaryReference(dictionary, key), incomingReferences, thisPass);
}
}
}
lastPass = thisPass;
thisPass = new HashSet<>();
}
return incomingReferences;
}
void addTarget(Reference reference, Map<COSBase, Collection<Reference>> incomingReferences, Set<COSBase> thisPass) {
COSBase object = reference.getTo();
if (object instanceof COSArray || object instanceof COSDictionary) {
Collection<Reference> incoming = incomingReferences.get(object);
if (incoming == null) {
incoming = new ArrayList<>();
incomingReferences.put(object, incoming);
thisPass.add(object);
}
incoming.add(reference);
}
}
( OptimizeAfterMerge вспомогательные методы findComplexObjects
и addTarget
)
interface Reference {
public COSBase getFrom();
public COSBase getTo();
public void setTo(COSBase to);
}
static class ArrayReference implements Reference {
public ArrayReference(COSArray array, int index) {
this.from = array;
this.index = index;
}
@Override
public COSBase getFrom() {
return from;
}
@Override
public COSBase getTo() {
return resolve(from.get(index));
}
@Override
public void setTo(COSBase to) {
from.set(index, to);
}
final COSArray from;
final int index;
}
static class DictionaryReference implements Reference {
public DictionaryReference(COSDictionary dictionary, COSName key) {
this.from = dictionary;
this.key = key;
}
@Override
public COSBase getFrom() {
return from;
}
@Override
public COSBase getTo() {
return resolve(from.getDictionaryObject(key));
}
@Override
public void setTo(COSBase to) {
from.setItem(key, to);
}
final COSDictionary from;
final COSName key;
}
( OptimizeAfterMerge вспомогательный интерфейс Reference
с реализациями ArrayReference
и DictionaryReference
)
И следующие вспомогательные методы и классы наконец идентифицируют и объединяют дубликаты:
int mergeDuplicates(Map<COSBase, Collection<Reference>> complexObjects) throws IOException {
List<HashOfCOSBase> hashes = new ArrayList<>(complexObjects.size());
for (COSBase object : complexObjects.keySet()) {
hashes.add(new HashOfCOSBase(object));
}
Collections.sort(hashes);
int removedDuplicates = 0;
if (!hashes.isEmpty()) {
int runStart = 0;
int runHash = hashes.get(0).hash;
for (int i = 1; i < hashes.size(); i++) {
int hash = hashes.get(i).hash;
if (hash != runHash) {
int runSize = i - runStart;
if (runSize != 1) {
System.out.printf("Equal hash %d for %d elements.\n", runHash, runSize);
removedDuplicates += mergeRun(complexObjects, hashes.subList(runStart, i));
}
runHash = hash;
runStart = i;
}
}
int runSize = hashes.size() - runStart;
if (runSize != 1) {
System.out.printf("Equal hash %d for %d elements.\n", runHash, runSize);
removedDuplicates += mergeRun(complexObjects, hashes.subList(runStart, hashes.size()));
}
}
return removedDuplicates;
}
int mergeRun(Map<COSBase, Collection<Reference>> complexObjects, List<HashOfCOSBase> run) {
int removedDuplicates = 0;
List<List<COSBase>> duplicateSets = new ArrayList<>();
for (HashOfCOSBase entry : run) {
COSBase element = entry.object;
for (List<COSBase> duplicateSet : duplicateSets) {
if (equals(element, duplicateSet.get(0))) {
duplicateSet.add(element);
element = null;
break;
}
}
if (element != null) {
List<COSBase> duplicateSet = new ArrayList<>();
duplicateSet.add(element);
duplicateSets.add(duplicateSet);
}
}
System.out.printf("Identified %d set(s) of identical objects in run.\n", duplicateSets.size());
for (List<COSBase> duplicateSet : duplicateSets) {
if (duplicateSet.size() > 1) {
COSBase surviver = duplicateSet.remove(0);
Collection<Reference> surviverReferences = complexObjects.get(surviver);
for (COSBase object : duplicateSet) {
Collection<Reference> references = complexObjects.get(object);
for (Reference reference : references) {
reference.setTo(surviver);
surviverReferences.add(reference);
}
complexObjects.remove(object);
removedDuplicates++;
}
surviver.setDirect(false);
}
}
return removedDuplicates;
}
boolean equals(COSBase a, COSBase b) {
if (a instanceof COSArray) {
if (b instanceof COSArray) {
COSArray aArray = (COSArray) a;
COSArray bArray = (COSArray) b;
if (aArray.size() == bArray.size()) {
for (int i=0; i < aArray.size(); i++) {
if (!resolve(aArray.get(i)).equals(resolve(bArray.get(i))))
return false;
}
return true;
}
}
} else if (a instanceof COSDictionary) {
if (b instanceof COSDictionary) {
COSDictionary aDict = (COSDictionary) a;
COSDictionary bDict = (COSDictionary) b;
Set<COSName> keys = aDict.keySet();
if (keys.equals(bDict.keySet())) {
for (COSName key : keys) {
if (!resolve(aDict.getItem(key)).equals(bDict.getItem(key)))
return false;
}
// In case of COSStreams we strictly speaking should
// also compare the stream contents here. But apparently
// their hashes coincide well enough for the original
// hashing equality, so let's just assume...
return true;
}
}
}
return false;
}
static COSBase resolve(COSBase object) {
while (object instanceof COSObject)
object = ((COSObject)object).getObject();
return object;
}
( OptimizeAfterMerge вспомогательные методы mergeDuplicates
mergeRun
equals
и resolve
)
static class HashOfCOSBase implements Comparable<HashOfCOSBase> {
public HashOfCOSBase(COSBase object) throws IOException {
this.object = object;
this.hash = calculateHash(object);
}
int calculateHash(COSBase object) throws IOException {
if (object instanceof COSArray) {
int result = 1;
for (COSBase member : (COSArray)object)
result = 31 * result + member.hashCode();
return result;
} else if (object instanceof COSDictionary) {
int result = 3;
for (Map.Entry<COSName, COSBase> entry : ((COSDictionary)object).entrySet())
result += entry.hashCode();
if (object instanceof COSStream) {
try ( InputStream data = ((COSStream)object).createRawInputStream() ) {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[8192];
int bytesRead = 0;
while((bytesRead = data.read(buffer)) >= 0)
md.update(buffer, 0, bytesRead);
result = 31 * result + Arrays.hashCode(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new IOException(e);
}
}
return result;
} else {
throw new IllegalArgumentException(String.format("Unknown complex COSBase type %s", object.getClass().getName()));
}
}
final COSBase object;
final int hash;
@Override
public int compareTo(HashOfCOSBase o) {
int result = Integer.compare(hash, o.hash);
if (result == 0)
result = Integer.compare(hashCode(), o.hashCode());
return result;
}
}
( OptimizeAfterMerge вспомогательный класс HashOfCOSBase
)
Применение кода к ОПпример документа
Пример документа OP составляет около 6,5 МБ.Применение приведенного выше кода, подобного следующему
PDDocument pdDocument = PDDocument.load(SOURCE);
optimize(pdDocument);
pdDocument.save(RESULT);
, приводит к получению PDF размером менее 700 КБ, и он выглядит завершенным.
(Если чего-то не хватает, пожалуйста, скажите, япопытайтесь это исправить.)
Слова предупреждения
С одной стороны, этот оптимизатор не распознает все идентичные дубликаты.В частности, в случае циклических ссылок дублированные круги объектов не будут распознаваться, потому что код распознает дубликаты, только если их содержимое идентично, что обычно не происходит в дублирующих кругах объектов.
С другой стороны, этот оптимизатор можетв некоторых случаях это может привести к чрезмерной нагрузке, поскольку некоторые дубликаты могут понадобиться в качестве отдельных объектов, чтобы программы просмотра PDF могли воспринимать каждый экземпляр как отдельную сущность.
Кроме того, эта программа затрагивает все виды объектов в файле, даже те, которые определяютвнутренние структуры PDF, но он не пытается обновить какие-либо классы PDFBox, управляющие этой структурой (PDDocument
, PDDocumentCatalog
, PDAcroForm
, ...).Чтобы не было никаких ожидающих изменений, можно испортить весь документ, поэтому, пожалуйста, применяйте эту программу только к недавно загруженным, неизмененным PDDocument
экземплярам и сохраняйте ее вскоре после этого без дальнейших церемоний.