Skip to content

Commit 68411e6

Browse files
committed
Fix append mode for hybrid-reference documents
iText used to refer to regular xref section by /Prev key in the xref stream which is "not meaningful in hybrid-reference files" according to the specification. In full compression mode after the changes in this commit first an xref stream is written and then a regular xref section referring to the xref stream. For safety, regular objects, not the ones from object streams, are also enumerated in the last regular xref section of the stamped document. DEVSIX-2080
1 parent 1a49101 commit 68411e6

File tree

4 files changed

+122
-57
lines changed

4 files changed

+122
-57
lines changed

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

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ This file is part of the iText (R) project.
4747
import com.itextpdf.io.source.ByteUtils;
4848
import com.itextpdf.io.util.MessageFormatUtil;
4949
import com.itextpdf.kernel.ProductInfo;
50-
import com.itextpdf.kernel.Version;
5150
import com.itextpdf.kernel.VersionInfo;
5251
import org.slf4j.Logger;
5352
import org.slf4j.LoggerFactory;
@@ -240,40 +239,14 @@ protected void writeXrefTableAndTrailer(PdfDocument document, PdfObject fileId,
240239
}
241240
}
242241

243-
List<Integer> sections = new ArrayList<>();
244-
int first = 0;
245-
int len = 0;
246-
for (int i = 0; i < size(); i++) {
247-
PdfIndirectReference reference = xref[i];
248-
if (document.properties.appendMode && reference != null && !reference.checkState(PdfObject.MODIFIED)) {
249-
reference = null;
250-
}
251-
252-
if (reference == null) {
253-
if (len > 0) {
254-
sections.add(first);
255-
sections.add(len);
256-
}
257-
len = 0;
258-
} else {
259-
if (len > 0) {
260-
len++;
261-
} else {
262-
first = i;
263-
len = 1;
264-
}
265-
}
266-
}
267-
if (len > 0) {
268-
sections.add(first);
269-
sections.add(len);
270-
}
242+
List<Integer> sections = createSections(document, false);
271243
if (document.properties.appendMode && sections.size() == 0) { // no modifications.
272244
xref = null;
273245
return;
274246
}
275247

276248
long startxref = writer.getCurrentPos();
249+
long xRefStmPos = -1;
277250
if (writer.isFullCompression()) {
278251
PdfStream xrefStream = (PdfStream) new PdfStream().makeIndirect(document);
279252
xrefStream.makeIndirect(document);
@@ -292,15 +265,16 @@ protected void writeXrefTableAndTrailer(PdfDocument document, PdfObject fileId,
292265
for (Integer section : sections) {
293266
index.add(new PdfNumber((int) section));
294267
}
295-
if (document.properties.appendMode) {
268+
if (document.properties.appendMode && !document.reader.hybridXref) {
269+
// "not meaningful in hybrid-reference files"
296270
PdfNumber lastXref = new PdfNumber(document.reader.getLastXref());
297271
xrefStream.put(PdfName.Prev, lastXref);
298272
}
299273
xrefStream.put(PdfName.Index, index);
300274
PdfXrefTable xrefTable = document.getXref();
301275
for (int k = 0; k < sections.size(); k += 2) {
302-
first = (int) sections.get(k);
303-
len = (int) sections.get(k + 1);
276+
int first = (int) sections.get(k);
277+
int len = (int) sections.get(k + 1);
304278
for (int i = first; i < first + len; i++) {
305279
PdfIndirectReference reference = xrefTable.get(i);
306280
if (reference.isFree()) {
@@ -319,12 +293,25 @@ protected void writeXrefTableAndTrailer(PdfDocument document, PdfObject fileId,
319293
}
320294
}
321295
xrefStream.flush();
322-
} else {
296+
xRefStmPos = startxref;
297+
}
298+
299+
// For documents with hybrid cross-reference table, i.e. containing xref streams as well as regular xref sections,
300+
// we write additional regular xref section at the end of the document because the /Prev reference from
301+
// xref stream to a regular xref section doesn't seem to be valid
302+
boolean needsRegularXref = !writer.isFullCompression() || document.properties.appendMode && document.reader.hybridXref;
303+
304+
if (needsRegularXref) {
305+
startxref = writer.getCurrentPos();
323306
writer.writeString("xref\n");
324307
PdfXrefTable xrefTable = document.getXref();
308+
if (xRefStmPos != -1) {
309+
// Get rid of all objects from object stream. This is done for hybrid documents
310+
sections = createSections(document, true);
311+
}
325312
for (int k = 0; k < sections.size(); k += 2) {
326-
first = (int) sections.get(k);
327-
len = (int) sections.get(k + 1);
313+
int first = (int) sections.get(k);
314+
int len = (int) sections.get(k + 1);
328315
writer.writeInteger(first).writeSpace().writeInteger(len).writeByte((byte) '\n');
329316
for (int i = first; i < first + len; i++) {
330317
PdfIndirectReference reference = xrefTable.get(i);
@@ -348,6 +335,9 @@ protected void writeXrefTableAndTrailer(PdfDocument document, PdfObject fileId,
348335
trailer.remove(PdfName.Length);
349336
trailer.put(PdfName.Size, new PdfNumber(this.size()));
350337
trailer.put(PdfName.ID, fileId);
338+
if (xRefStmPos != -1) {
339+
trailer.put(PdfName.XRefStm, new PdfNumber(xRefStmPos));
340+
}
351341
if (crypto != null)
352342
trailer.put(PdfName.Encrypt, crypto);
353343
writer.writeString("trailer\n");
@@ -376,6 +366,40 @@ void clear() {
376366
count = 1;
377367
}
378368

369+
private List<Integer> createSections(PdfDocument document, boolean dropObjectsFromObjectStream) {
370+
List<Integer> sections = new ArrayList<>();
371+
int first = 0;
372+
int len = 0;
373+
for (int i = 0; i < size(); i++) {
374+
PdfIndirectReference reference = xref[i];
375+
if (document.properties.appendMode && reference != null &&
376+
(!reference.checkState(PdfObject.MODIFIED) || dropObjectsFromObjectStream && reference.getObjStreamNumber() != 0)) {
377+
reference = null;
378+
}
379+
380+
if (reference == null) {
381+
if (len > 0) {
382+
sections.add(first);
383+
sections.add(len);
384+
}
385+
len = 0;
386+
} else {
387+
if (len > 0) {
388+
len++;
389+
} else {
390+
first = i;
391+
len = 1;
392+
}
393+
}
394+
}
395+
if (len > 0) {
396+
sections.add(first);
397+
sections.add(len);
398+
}
399+
400+
return sections;
401+
}
402+
379403
/**
380404
* Gets size of the offset. Max size is 2^40, i.e. 1 Tb.
381405
*/

sign/src/test/java/com/itextpdf/signatures/sign/PdfSignatureAppearanceTest.java

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ This file is part of the iText (R) project.
7979
import java.security.UnrecoverableKeyException;
8080
import java.security.cert.Certificate;
8181
import java.security.cert.CertificateException;
82+
import java.util.Arrays;
83+
import java.util.HashMap;
84+
import java.util.List;
85+
import java.util.Map;
8286

8387
@Category(IntegrationTest.class)
8488
public class PdfSignatureAppearanceTest extends ExtendedITextTest {
@@ -169,6 +173,61 @@ public void textAutoscaleTest06() throws GeneralSecurityException, IOException,
169173
assertAppearanceFontSize(dest, 6.26f);
170174
}
171175

176+
@Test
177+
public void testSigningInAppendModeWithHybridDocument() throws IOException, GeneralSecurityException, InterruptedException {
178+
String src = sourceFolder + "hybrid.pdf";
179+
String dest = destinationFolder + "signed_hybrid.pdf";
180+
String cmp = sourceFolder + "cmp_signed_hybrid.pdf";
181+
182+
PdfSigner signer = new PdfSigner(new PdfReader(src), new FileOutputStream(dest), new StampingProperties().useAppendMode());
183+
184+
PdfSignatureAppearance appearance = signer.getSignatureAppearance();
185+
186+
appearance.setLayer2FontSize(13.8f)
187+
.setPageRect(new Rectangle(36, 748, 200, 100))
188+
.setPageNumber(1)
189+
.setReason("Test")
190+
.setLocation("Nagpur");
191+
192+
signer.setFieldName("Sign1");
193+
194+
signer.setCertificationLevel(PdfSigner.NOT_CERTIFIED);
195+
196+
IExternalSignature pks = new PrivateKeySignature(pk, DigestAlgorithms.SHA256, BouncyCastleProvider.PROVIDER_NAME);
197+
signer.signDetached(new BouncyCastleDigest(), pks, chain, null, null, null, 0, PdfSigner.CryptoStandard.CADES);
198+
199+
// Make sure iText can open the document
200+
new PdfDocument(new PdfReader(dest)).close();
201+
202+
// Assert that the document can be rendered correctly
203+
Assert.assertNull(new CompareTool().compareVisually(dest, cmp, destinationFolder, "diff_",
204+
getIgnoredAreaTestMap(new Rectangle(36, 748, 200, 100))));
205+
}
206+
207+
@Test
208+
public void fontColorTest01() throws GeneralSecurityException, IOException, InterruptedException {
209+
String fileName = "fontColorTest01.pdf";
210+
String dest = destinationFolder + fileName;
211+
212+
Rectangle rect = new Rectangle(36, 648, 100, 50);
213+
String src = sourceFolder + "simpleDocument.pdf";
214+
215+
PdfSigner signer = new PdfSigner(new PdfReader(src), new FileOutputStream(dest), new StampingProperties());
216+
// Creating the appearance
217+
signer.getSignatureAppearance()
218+
.setLayer2FontColor(ColorConstants.RED)
219+
.setLayer2Text("Verified and signed by me.")
220+
.setPageRect(rect);
221+
222+
signer.setFieldName("Signature1");
223+
// Creating the signature
224+
IExternalSignature pks = new PrivateKeySignature(pk, DigestAlgorithms.SHA256, BouncyCastleProvider.PROVIDER_NAME);
225+
signer.signDetached(new BouncyCastleDigest(), pks, chain, null, null, null, 0, PdfSigner.CryptoStandard.CADES);
226+
227+
Assert.assertNull(new CompareTool().compareVisually(dest, sourceFolder + "cmp_" + fileName, destinationFolder,
228+
"diff_"));
229+
}
230+
172231
private void testSignatureAppearanceAutoscale(String dest, Rectangle rect, PdfSignatureAppearance.RenderingMode renderingMode) throws IOException, GeneralSecurityException {
173232
String src = sourceFolder + "simpleDocument.pdf";
174233

@@ -206,28 +265,10 @@ private static void assertAppearanceFontSize(String filename, float expectedFont
206265
Assert.assertTrue(MessageFormatUtil.format("Font size: exptected {0}, found {1}", expectedFontSize, fontSize), Math.abs(foundFontSize - expectedFontSize) < 0.1 * expectedFontSize);
207266
}
208267

209-
@Test
210-
public void fontColorTest01() throws GeneralSecurityException, IOException, InterruptedException {
211-
String fileName = "fontColorTest01.pdf";
212-
String dest = destinationFolder + fileName;
213-
214-
Rectangle rect = new Rectangle(36, 648, 100, 50);
215-
String src = sourceFolder + "simpleDocument.pdf";
216-
217-
PdfSigner signer = new PdfSigner(new PdfReader(src), new FileOutputStream(dest), new StampingProperties());
218-
// Creating the appearance
219-
signer.getSignatureAppearance()
220-
.setLayer2FontColor(ColorConstants.RED)
221-
.setLayer2Text("Verified and signed by me.")
222-
.setPageRect(rect);
223-
224-
signer.setFieldName("Signature1");
225-
// Creating the signature
226-
IExternalSignature pks = new PrivateKeySignature(pk, DigestAlgorithms.SHA256, BouncyCastleProvider.PROVIDER_NAME);
227-
signer.signDetached(new BouncyCastleDigest(), pks, chain, null, null, null, 0, PdfSigner.CryptoStandard.CADES);
228-
229-
Assert.assertNull(new CompareTool().compareVisually(dest, sourceFolder + "cmp_" + fileName, destinationFolder,
230-
"diff_"));
268+
private static Map<Integer, List<Rectangle>> getIgnoredAreaTestMap(Rectangle ignoredArea) {
269+
Map<Integer, List<Rectangle>> result = new HashMap<Integer, List<Rectangle>>();
270+
result.put(1, Arrays.asList(ignoredArea));
271+
return result;
231272
}
232273

233274
}

0 commit comments

Comments
 (0)