Skip to content

Commit 67ec4c2

Browse files
author
Dmitry Radchuk
committed
Add script merging
DEVSIX-7752
1 parent 94a1ad0 commit 67ec4c2

22 files changed

+476
-10
lines changed

kernel/src/main/java/com/itextpdf/kernel/exceptions/KernelExceptionMessageConstant.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public final class KernelExceptionMessageConstant {
112112
+ "encrypted payload to a document opened in read only mode.";
113113
public static final String CANNOT_SET_ENCRYPTED_PAYLOAD_TO_ENCRYPTED_DOCUMENT = "Cannot set encrypted payload "
114114
+ "to an encrypted document.";
115+
115116
public static final String CANNOT_SPLIT_DOCUMENT_THAT_IS_BEING_WRITTEN = "Cannot split document that is "
116117
+ "being written.";
117118
public static final String CANNOT_WRITE_TO_PDF_STREAM = "Cannot write to PdfStream.";

kernel/src/main/java/com/itextpdf/kernel/logs/KernelLogMessageConstant.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public final class KernelLogMessageConstant {
6060

6161
public static final String UNABLE_TO_PARSE_COLOR_WITHIN_COLORSPACE =
6262
"Unable to parse color {0} within {1} color space";
63+
public static final String CANNOT_MERGE_ENTRY = "Cannot merge entry {0}, entry with such key already exists.";
6364

6465
/**
6566
* Message warns about unexpected product name which was mentioned as involved into PDF

kernel/src/main/java/com/itextpdf/kernel/pdf/PdfCatalog.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,17 @@ public PdfNameTree getNameTree(PdfName treeType) {
296296
return tree;
297297
}
298298

299+
/**
300+
* This method checks Names tree for specified tree type.
301+
*
302+
* @param treeType type of tree which existence should be checked
303+
*
304+
* @return true if such tree exists, false otherwise
305+
*/
306+
public boolean nameTreeContainsKey(PdfName treeType) {
307+
return nameTrees.containsKey(treeType);
308+
}
309+
299310
/**
300311
* This method returns the NumberTree of Page Labels
301312
*

kernel/src/main/java/com/itextpdf/kernel/utils/PdfMerger.java

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.kernel.utils;
2424

25+
import com.itextpdf.kernel.pdf.IPdfPageExtraCopier;
2526
import com.itextpdf.kernel.pdf.PdfDocument;
2627

2728
import java.util.ArrayList;
@@ -31,9 +32,7 @@ This file is part of the iText (R) project.
3132
public class PdfMerger {
3233

3334
private PdfDocument pdfDocument;
34-
private boolean closeSrcDocuments;
35-
private boolean mergeTags;
36-
private boolean mergeOutlines;
35+
private PdfMergerProperties properties;
3736

3837
/**
3938
* This class is used to merge a number of existing documents into one. By default, if source document
@@ -55,11 +54,25 @@ public PdfMerger(PdfDocument pdfDocument) {
5554
* @param mergeOutlines if true, then outlines from the source document are copied even if in destination document
5655
* outlines are not initialized. Note, that if false, outlines are still could be copied if the
5756
* destination document outlines were explicitly initialized with {@link PdfDocument#initializeOutlines()}
57+
*
58+
* @deprecated use <code>PdfMerger(PdfDocument, PdfMergerProperties)</code> constructor
5859
*/
60+
@Deprecated
5961
public PdfMerger(PdfDocument pdfDocument, boolean mergeTags, boolean mergeOutlines) {
6062
this.pdfDocument = pdfDocument;
61-
this.mergeTags = mergeTags;
62-
this.mergeOutlines = mergeOutlines;
63+
this.properties = new PdfMergerProperties();
64+
this.properties.setMergeTags(mergeTags).setMergeOutlines(mergeOutlines);
65+
}
66+
67+
/**
68+
* This class is used to merge a number of existing documents into one.
69+
*
70+
* @param pdfDocument the document into which source documents will be merged
71+
* @param properties properties for the created <code>PdfMerger</code>
72+
*/
73+
public PdfMerger(PdfDocument pdfDocument, PdfMergerProperties properties) {
74+
this.pdfDocument = pdfDocument;
75+
this.properties = properties != null ? properties : new PdfMergerProperties();
6376
}
6477

6578
/**
@@ -71,7 +84,7 @@ public PdfMerger(PdfDocument pdfDocument, boolean mergeTags, boolean mergeOutlin
7184
* @return this {@code PdfMerger} instance
7285
*/
7386
public PdfMerger setCloseSourceDocuments(boolean closeSourceDocuments) {
74-
this.closeSrcDocuments = closeSourceDocuments;
87+
this.properties.setCloseSrcDocuments(closeSourceDocuments);
7588
return this;
7689
}
7790

@@ -109,15 +122,37 @@ public PdfMerger merge(PdfDocument from, int fromPage, int toPage) {
109122
* @return this {@code PdfMerger} instance
110123
*/
111124
public PdfMerger merge(PdfDocument from, List<Integer> pages) {
112-
if (mergeTags && from.isTagged()) {
125+
return merge(from, pages, null);
126+
}
127+
128+
/**
129+
* This method merges pages from the source document to the current one.
130+
* <p>
131+
* If <i>closeSourceDocuments</i> flag is set to <i>true</i> (see {@link #setCloseSourceDocuments(boolean)}),
132+
* passed {@code PdfDocument} will be closed after pages are merged.
133+
* <p>
134+
* See also {@link com.itextpdf.kernel.pdf.PdfDocument#copyPagesTo}.
135+
*
136+
* @param from - document, from which pages will be copied
137+
* @param pages - List of numbers of pages which will be copied
138+
* @param copier - a copier which bears a special copy logic. May be null.
139+
* It is recommended to use the same instance of {@link IPdfPageExtraCopier}
140+
* for the same output document.
141+
* @return this {@code PdfMerger} instance
142+
*/
143+
public PdfMerger merge(PdfDocument from, List<Integer> pages, IPdfPageExtraCopier copier) {
144+
if (properties.isMergeTags() && from.isTagged()) {
113145
pdfDocument.setTagged();
114146
}
115-
if (mergeOutlines && from.hasOutlines()) {
147+
if (properties.isMergeOutlines() && from.hasOutlines()) {
116148
pdfDocument.initializeOutlines();
117149
}
150+
if (properties.isMergeScripts()) {
151+
PdfScriptMerger.mergeScripts(from, this.pdfDocument);
152+
}
118153

119-
from.copyPagesTo(pages, pdfDocument);
120-
if (closeSrcDocuments) {
154+
from.copyPagesTo(pages, pdfDocument, copier);
155+
if (properties.isCloseSrcDocuments()) {
121156
from.close();
122157
}
123158
return this;
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2023 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.kernel.utils;
24+
25+
/**
26+
* Class with additional properties for {@link PdfMerger} processing.
27+
* Needs to be passed at merger initialization.
28+
*/
29+
public class PdfMergerProperties {
30+
private boolean closeSrcDocuments;
31+
private boolean mergeTags;
32+
private boolean mergeOutlines;
33+
private boolean mergeScripts;
34+
35+
/**
36+
* Default constructor, use provided setters for configuration options.
37+
*/
38+
public PdfMergerProperties() {
39+
closeSrcDocuments = false;
40+
mergeTags = true;
41+
mergeOutlines = true;
42+
mergeScripts = false;
43+
}
44+
45+
/**
46+
* check if source documents should be close after merging
47+
*
48+
* @return true if they should, false otherwise
49+
*/
50+
public boolean isCloseSrcDocuments() {
51+
return closeSrcDocuments;
52+
}
53+
54+
/**
55+
* check if tags should be merged
56+
*
57+
* @return true if they should, false otherwise
58+
*/
59+
public boolean isMergeTags() {
60+
return mergeTags;
61+
}
62+
63+
/**
64+
* check if outlines should be merged
65+
*
66+
* @return true if they should, false otherwise
67+
*/
68+
public boolean isMergeOutlines() {
69+
return mergeOutlines;
70+
}
71+
72+
/**
73+
* check if ECMA scripts (which are executed at document opening) should be merged
74+
*
75+
* @return true if they should, false otherwise
76+
*/
77+
public boolean isMergeScripts() {
78+
return mergeScripts;
79+
}
80+
81+
/**
82+
* close source documents after merging
83+
*
84+
* @param closeSrcDocuments true to close, false otherwise
85+
*
86+
* @return <code>PdfMergerProperties</code> instance
87+
*/
88+
public PdfMergerProperties setCloseSrcDocuments(boolean closeSrcDocuments) {
89+
this.closeSrcDocuments = closeSrcDocuments;
90+
return this;
91+
}
92+
93+
/**
94+
* merge documents tags
95+
*
96+
* @param mergeTags true to merge, false otherwise
97+
*
98+
* @return <code>PdfMergerProperties</code> instance
99+
*/
100+
public PdfMergerProperties setMergeTags(boolean mergeTags) {
101+
this.mergeTags = mergeTags;
102+
return this;
103+
}
104+
105+
/**
106+
* merge documents outlines
107+
*
108+
* @param mergeOutlines true to merge, false otherwise
109+
*
110+
* @return <code>PdfMergerProperties</code> instance
111+
*/
112+
public PdfMergerProperties setMergeOutlines(boolean mergeOutlines) {
113+
this.mergeOutlines = mergeOutlines;
114+
return this;
115+
}
116+
117+
/**
118+
* merge documents ECMA scripts
119+
*
120+
* @param mergeNames true to merge, false otherwise
121+
*
122+
* @return <code>PdfMergerProperties</code> instance
123+
*/
124+
public PdfMergerProperties setMergeScripts(boolean mergeNames) {
125+
this.mergeScripts = mergeNames;
126+
return this;
127+
}
128+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package com.itextpdf.kernel.utils;
2+
3+
import com.itextpdf.commons.utils.MessageFormatUtil;
4+
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
5+
import com.itextpdf.kernel.pdf.PdfArray;
6+
import com.itextpdf.kernel.pdf.PdfDictionary;
7+
import com.itextpdf.kernel.pdf.PdfDocument;
8+
import com.itextpdf.kernel.pdf.PdfName;
9+
import com.itextpdf.kernel.pdf.PdfNameTree;
10+
import com.itextpdf.kernel.pdf.PdfObject;
11+
import com.itextpdf.kernel.pdf.PdfString;
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
15+
import java.util.Arrays;
16+
import java.util.Collections;
17+
import java.util.HashSet;
18+
import java.util.Map;
19+
import java.util.Set;
20+
21+
/**
22+
* Utility class which provides functionality to merge ECMA scripts from pdf documents
23+
*/
24+
public class PdfScriptMerger {
25+
private static final Logger LOGGER = LoggerFactory.getLogger(PdfScriptMerger.class);
26+
private static final Set<PdfName> allowedAAEntries = Collections
27+
.unmodifiableSet(new HashSet<>(Arrays.asList(
28+
PdfName.WC,
29+
PdfName.WS,
30+
PdfName.DS,
31+
PdfName.WP)));
32+
33+
/**
34+
* Merges ECMA scripts from source to destinations from all possible places for them,
35+
* it only copies first action in chain for AA and OpenAction entries
36+
*
37+
* @param source source document from which script will be copied
38+
* @param destination destination document to which script will be copied
39+
*/
40+
public static void mergeScripts(PdfDocument source, PdfDocument destination) {
41+
mergeOpenActionsScripts(source, destination);
42+
mergeAdditionalActionsScripts(source, destination);
43+
mergeNamesScripts(source, destination);
44+
}
45+
46+
/**
47+
* Copies AA catalog entry ECMA scripts, it only copies first action in chain
48+
*
49+
* @param source source document from which script will be copied
50+
* @param destination destination document to which script will be copied
51+
*/
52+
public static void mergeAdditionalActionsScripts(PdfDocument source, PdfDocument destination) {
53+
PdfDictionary sourceAA = source.getCatalog().getPdfObject().getAsDictionary(PdfName.AA);
54+
PdfDictionary destinationAA = destination.getCatalog().getPdfObject().getAsDictionary(PdfName.AA);
55+
if (sourceAA == null || sourceAA.isEmpty()) {
56+
return;
57+
}
58+
if (destinationAA == null) {
59+
destinationAA = new PdfDictionary();
60+
destination.getCatalog().getPdfObject().put(PdfName.AA, destinationAA);
61+
}
62+
for (Map.Entry<PdfName, PdfObject> entry : sourceAA.entrySet()) {
63+
if (destinationAA.containsKey(entry.getKey())) {
64+
LOGGER.error(MessageFormatUtil.format(KernelLogMessageConstant.CANNOT_MERGE_ENTRY, entry.getKey()));
65+
return;
66+
}
67+
if (!allowedAAEntries.contains(entry.getKey())) {
68+
continue;
69+
}
70+
destinationAA.put(entry.getKey(), copyECMAScriptActionsDictionary(destination, (PdfDictionary) entry.getValue()));
71+
}
72+
}
73+
74+
/**
75+
* Copies open actions catalog entry ECMA scripts, it only copies first action in chain
76+
*
77+
* @param source source document from which script will be copied
78+
* @param destination destination document to which script will be copied
79+
*/
80+
public static void mergeOpenActionsScripts(PdfDocument source, PdfDocument destination) {
81+
PdfObject sourceOpenAction = source.getCatalog().getPdfObject().get(PdfName.OpenAction);
82+
if (sourceOpenAction instanceof PdfArray) {
83+
return;
84+
}
85+
PdfDictionary sourceOpenActionDict = source.getCatalog().getPdfObject().getAsDictionary(PdfName.OpenAction);
86+
if (sourceOpenActionDict == null || sourceOpenActionDict.isEmpty() || !PdfName.JavaScript.equals(sourceOpenActionDict.get(PdfName.S))) {
87+
return;
88+
}
89+
PdfObject destinationOpenAction = destination.getCatalog().getPdfObject().get(PdfName.OpenAction);
90+
if (destinationOpenAction != null) {
91+
LOGGER.error(MessageFormatUtil.format(KernelLogMessageConstant.CANNOT_MERGE_ENTRY, PdfName.OpenAction));
92+
return;
93+
}
94+
destination.getCatalog().getPdfObject().put(PdfName.OpenAction, copyECMAScriptActionsDictionary(destination, sourceOpenActionDict));
95+
}
96+
97+
/**
98+
* Copies ECMA scripts from Names catalog entry
99+
*
100+
* @param source source document from which script will be copied
101+
* @param destination destination document to which script will be copied
102+
*/
103+
public static void mergeNamesScripts(PdfDocument source, PdfDocument destination) {
104+
PdfDictionary namesDict = source.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
105+
if (namesDict == null || !namesDict.containsKey(PdfName.JavaScript)) {
106+
return;
107+
}
108+
PdfDictionary destinationNamesDict = destination.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
109+
if ((destinationNamesDict != null && destinationNamesDict.get(PdfName.JavaScript) != null)
110+
|| destination.getCatalog().nameTreeContainsKey(PdfName.JavaScript)) {
111+
LOGGER.error(MessageFormatUtil.format(KernelLogMessageConstant.CANNOT_MERGE_ENTRY, PdfName.JavaScript));
112+
return;
113+
}
114+
115+
116+
PdfNameTree sourceTree = new PdfNameTree(source.getCatalog(), PdfName.JavaScript);
117+
PdfNameTree destinationTree = destination.getCatalog().getNameTree(PdfName.JavaScript);
118+
for (Map.Entry<PdfString, PdfObject> entry : sourceTree.getNames().entrySet()) {
119+
PdfDictionary ECMAScriptActionsDirectCopy = copyECMAScriptActionsDictionary(destination,
120+
entry.getValue().isIndirect() ? (PdfDictionary) entry.getValue().getIndirectReference().getRefersTo()
121+
: (PdfDictionary) entry.getValue());
122+
destinationTree.addEntry(entry.getKey(), ECMAScriptActionsDirectCopy);
123+
}
124+
}
125+
126+
private static PdfDictionary copyECMAScriptActionsDictionary(PdfDocument destination, PdfDictionary actions) {
127+
PdfObject originalScriptSource = actions.get(PdfName.JS);
128+
PdfObject scriptType = actions.get(PdfName.S);
129+
PdfDictionary actionsCopy = new PdfDictionary();
130+
if (originalScriptSource != null) {
131+
actionsCopy.put(PdfName.JS, originalScriptSource.copyTo(destination));
132+
}
133+
if (scriptType != null) {
134+
actionsCopy.put(PdfName.S, scriptType.copyTo(destination));
135+
}
136+
return actionsCopy;
137+
}
138+
}

0 commit comments

Comments
 (0)