Skip to content

Commit 532081a

Browse files
author
Dmitry Radchuk
committed
Add API for getting PdfLayers from page
DEVSIX-8576
1 parent 2094194 commit 532081a

File tree

6 files changed

+221
-23
lines changed

6 files changed

+221
-23
lines changed

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

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,59 @@ private OcgPropertiesCopier() {
4646
// Empty constructor
4747
}
4848

49-
public static void copyOCGProperties(PdfDocument fromDocument, PdfDocument toDocument, Map<PdfPage, PdfPage> page2page) {
49+
/**
50+
* Copy unique page OCGs stored inside annotations/xobjects/resources from source pages to destination pages.
51+
*
52+
* @param sourceDocument document from which OCGs should be copied
53+
* @param destinationDocument document to which OCGs should be copied
54+
* @param sourceToDestPageMapping page mapping, linking source pages to destination ones
55+
*/
56+
public static void copyOCGProperties(PdfDocument sourceDocument, PdfDocument destinationDocument,
57+
Map<PdfPage, PdfPage> sourceToDestPageMapping) {
5058
try {
5159
// Configs are not copied
52-
53-
PdfDictionary toOcProperties = toDocument.getCatalog().getPdfObject().getAsDictionary(PdfName.OCProperties);
54-
final Set<PdfIndirectReference> fromOcgsToCopy = OcgPropertiesCopier
55-
.getAllUsedNonFlushedOCGs(page2page, toOcProperties);
56-
if (fromOcgsToCopy.isEmpty()) {
60+
PdfDictionary toOcProperties = destinationDocument.getCatalog().getPdfObject().getAsDictionary(PdfName.OCProperties);
61+
final Set<PdfIndirectReference> ocgsToCopy = OcgPropertiesCopier
62+
.getAllUsedNonFlushedOCGs(sourceToDestPageMapping, toOcProperties);
63+
if (ocgsToCopy.isEmpty()) {
5764
return;
5865
}
5966

6067
// Reset ocProperties field in order to create it a new at the
6168
// method end using the new (merged) OCProperties dictionary
62-
toOcProperties = toDocument.getCatalog().fillAndGetOcPropertiesDictionary();
63-
final PdfDictionary fromOcProperties = fromDocument.getCatalog().getPdfObject()
69+
toOcProperties = destinationDocument.getCatalog().fillAndGetOcPropertiesDictionary();
70+
final PdfDictionary fromOcProperties = sourceDocument.getCatalog().getPdfObject()
6471
.getAsDictionary(PdfName.OCProperties);
6572

66-
OcgPropertiesCopier.copyOCGs(fromOcgsToCopy, toOcProperties, toDocument);
73+
OcgPropertiesCopier.copyOCGs(ocgsToCopy, toOcProperties, destinationDocument);
6774

68-
OcgPropertiesCopier.copyDDictionary(fromOcgsToCopy, fromOcProperties.getAsDictionary(PdfName.D),
69-
toOcProperties, toDocument);
75+
OcgPropertiesCopier.copyDDictionary(ocgsToCopy, fromOcProperties.getAsDictionary(PdfName.D),
76+
toOcProperties, destinationDocument);
7077
} catch (Exception e) {
7178
LOGGER.error(MessageFormatUtil.format(IoLogMessageConstant.OCG_COPYING_ERROR, e.toString()));
7279
}
7380
}
7481

82+
/**
83+
* Get all OCGs from a given page annotations/xobjects/resources, including ones already stored in catalog
84+
*
85+
* @param page where to search for OCGs.
86+
* @return set of indirect references pointing to found OCGs.
87+
*/
88+
public static Set<PdfIndirectReference> getOCGsFromPage(PdfPage page) {
89+
//Using linked hash set for elements order consistency (e.g. in tests)
90+
final Set<PdfIndirectReference> ocgs = new LinkedHashSet<>();
91+
final List<PdfAnnotation> annotations = page.getAnnotations();
92+
for (PdfAnnotation annotation : annotations) {
93+
//Pass null instead of catalog OCProperties value, to include ocg clashing with catalog
94+
getUsedNonFlushedOCGsFromAnnotation(null, ocgs, annotation, annotation);
95+
}
96+
final PdfDictionary resources = page.getPdfObject().getAsDictionary(PdfName.Resources);
97+
OcgPropertiesCopier.getUsedNonFlushedOCGsFromResources(resources, resources, ocgs,
98+
null, new HashSet<>());
99+
return ocgs;
100+
}
101+
75102
private static Set<PdfIndirectReference> getAllUsedNonFlushedOCGs(Map<PdfPage, PdfPage> page2page, PdfDictionary toOcProperties) {
76103
// NOTE: the PDF is considered to be valid and therefore the presence of OСG in OCProperties.OCGs is not checked
77104
final Set<PdfIndirectReference> fromUsedOcgs = new LinkedHashSet<>();
@@ -87,20 +114,10 @@ private static Set<PdfIndirectReference> getAllUsedNonFlushedOCGs(Map<PdfPage, P
87114
final List<PdfAnnotation> fromAnnotations = fromPage.getAnnotations();
88115
for (int j = 0; j < toAnnotations.size(); j++) {
89116
if (!toAnnotations.get(j).isFlushed()) {
90-
final PdfDictionary toAnnotDict = toAnnotations.get(j).getPdfObject();
91-
final PdfDictionary fromAnnotDict = fromAnnotations.get(j).getPdfObject();
92117
final PdfAnnotation toAnnot = toAnnotations.get(j);
93118
final PdfAnnotation fromAnnot = fromAnnotations.get(j);
94-
if (!toAnnotDict.isFlushed()) {
95-
OcgPropertiesCopier.getUsedNonFlushedOCGsFromOcDict(toAnnotDict.getAsDictionary(PdfName.OC),
96-
fromAnnotDict.getAsDictionary(PdfName.OC), fromUsedOcgs, toOcProperties);
97-
98-
OcgPropertiesCopier.getUsedNonFlushedOCGsFromXObject(toAnnot.getNormalAppearanceObject(),
99-
fromAnnot.getNormalAppearanceObject(), fromUsedOcgs, toOcProperties, new HashSet<>());
100-
OcgPropertiesCopier.getUsedNonFlushedOCGsFromXObject(toAnnot.getRolloverAppearanceObject(),
101-
fromAnnot.getRolloverAppearanceObject(), fromUsedOcgs, toOcProperties, new HashSet<>());
102-
OcgPropertiesCopier.getUsedNonFlushedOCGsFromXObject(toAnnot.getDownAppearanceObject(),
103-
fromAnnot.getDownAppearanceObject(), fromUsedOcgs, toOcProperties, new HashSet<>());
119+
if (!toAnnot.getPdfObject().isFlushed()) {
120+
getUsedNonFlushedOCGsFromAnnotation(toOcProperties, fromUsedOcgs, toAnnot, fromAnnot);
104121
}
105122
}
106123
}
@@ -113,6 +130,17 @@ private static Set<PdfIndirectReference> getAllUsedNonFlushedOCGs(Map<PdfPage, P
113130
return fromUsedOcgs;
114131
}
115132

133+
private static void getUsedNonFlushedOCGsFromAnnotation(PdfDictionary toOcProperties, Set<PdfIndirectReference> fromUsedOcgs, PdfAnnotation toAnnot, PdfAnnotation fromAnnot) {
134+
OcgPropertiesCopier.getUsedNonFlushedOCGsFromOcDict(toAnnot.getPdfObject().getAsDictionary(PdfName.OC),
135+
fromAnnot.getPdfObject().getAsDictionary(PdfName.OC), fromUsedOcgs, toOcProperties);
136+
OcgPropertiesCopier.getUsedNonFlushedOCGsFromXObject(toAnnot.getNormalAppearanceObject(),
137+
fromAnnot.getNormalAppearanceObject(), fromUsedOcgs, toOcProperties, new HashSet<>());
138+
OcgPropertiesCopier.getUsedNonFlushedOCGsFromXObject(toAnnot.getRolloverAppearanceObject(),
139+
fromAnnot.getRolloverAppearanceObject(), fromUsedOcgs, toOcProperties, new HashSet<>());
140+
OcgPropertiesCopier.getUsedNonFlushedOCGsFromXObject(toAnnot.getDownAppearanceObject(),
141+
fromAnnot.getDownAppearanceObject(), fromUsedOcgs, toOcProperties, new HashSet<>());
142+
}
143+
116144
private static void getUsedNonFlushedOCGsFromResources(PdfDictionary toResources, PdfDictionary fromResources,
117145
Set<PdfIndirectReference> fromUsedOcgs, PdfDictionary toOcProperties, Set<PdfObject> visitedObjects) {
118146
if (toResources != null && !toResources.isFlushed()) {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ This file is part of the iText (R) project.
3636
import com.itextpdf.kernel.pdf.annot.PdfPrinterMarkAnnotation;
3737
import com.itextpdf.kernel.pdf.annot.PdfWidgetAnnotation;
3838
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
39+
import com.itextpdf.kernel.pdf.layer.PdfLayer;
3940
import com.itextpdf.kernel.pdf.tagging.PdfStructTreeRoot;
4041
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
4142
import com.itextpdf.kernel.pdf.tagutils.TagStructureContext;
@@ -53,7 +54,11 @@ This file is part of the iText (R) project.
5354
import java.io.IOException;
5455
import java.util.ArrayList;
5556
import java.util.Arrays;
57+
import java.util.HashSet;
58+
import java.util.LinkedHashSet;
5659
import java.util.List;
60+
import java.util.Set;
61+
5762
import org.slf4j.Logger;
5863
import org.slf4j.LoggerFactory;
5964

@@ -421,6 +426,24 @@ public PdfPage copyTo(PdfDocument toDocument, IPdfPageExtraCopier copier,
421426
return copyTo(page, toDocument, copier);
422427
}
423428

429+
/**
430+
* Get all pdf layers stored under this page's annotations/xobjects/resources.
431+
* Note that it will include all layers, even those already stored under /OCProperties entry in catalog.
432+
* To get only unique layers, you can simply exclude ocgs, which already present in catalog.
433+
*
434+
* @return set of pdf layers, associated with this page.
435+
*/
436+
public Set<PdfLayer> getPdfLayers() {
437+
Set<PdfIndirectReference> ocgs = OcgPropertiesCopier.getOCGsFromPage(this);
438+
Set<PdfLayer> result = new LinkedHashSet<>();
439+
for (PdfIndirectReference ocg : ocgs) {
440+
if (ocg.getRefersTo() != null && ocg.getRefersTo().isDictionary()) {
441+
result.add(new PdfLayer((PdfDictionary) ocg.getRefersTo()));
442+
}
443+
}
444+
return result;
445+
}
446+
424447
/**
425448
* Copies page as FormXObject to the specified document.
426449
*

kernel/src/test/java/com/itextpdf/kernel/pdf/layer/PdfLayerTest.java

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,37 @@ This file is part of the iText (R) project.
2424

2525
import com.itextpdf.commons.utils.MessageFormatUtil;
2626
import com.itextpdf.io.font.constants.StandardFonts;
27+
import com.itextpdf.io.image.ImageData;
28+
import com.itextpdf.io.image.ImageDataFactory;
2729
import com.itextpdf.io.source.ByteArrayOutputStream;
2830
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
2931
import com.itextpdf.kernel.exceptions.PdfException;
3032
import com.itextpdf.kernel.font.PdfFont;
3133
import com.itextpdf.kernel.font.PdfFontFactory;
34+
import com.itextpdf.kernel.geom.Rectangle;
35+
import com.itextpdf.kernel.pdf.OcgPropertiesCopierTest;
3236
import com.itextpdf.kernel.pdf.PdfDictionary;
3337
import com.itextpdf.kernel.pdf.PdfDocument;
3438
import com.itextpdf.kernel.pdf.PdfName;
39+
import com.itextpdf.kernel.pdf.PdfPage;
3540
import com.itextpdf.kernel.pdf.PdfReader;
41+
import com.itextpdf.kernel.pdf.PdfResources;
3642
import com.itextpdf.kernel.pdf.PdfWriter;
43+
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
44+
import com.itextpdf.kernel.pdf.annot.PdfTextAnnotation;
3745
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
3846
import com.itextpdf.kernel.pdf.PdfIndirectReference;
47+
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
48+
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;
3949
import com.itextpdf.kernel.utils.CompareTool;
4050
import com.itextpdf.test.ExtendedITextTest;
4151
import java.io.IOException;
4252
import java.util.ArrayList;
4353
import java.util.Collection;
4454
import java.util.Collections;
4555
import java.util.List;
56+
import java.util.Set;
57+
4658
import org.junit.jupiter.api.AfterAll;
4759
import org.junit.jupiter.api.Assertions;
4860
import org.junit.jupiter.api.BeforeAll;
@@ -440,6 +452,141 @@ public void testInStamperMode2() throws IOException, InterruptedException {
440452
Assertions.assertNull(new CompareTool().compareByContent(destinationFolder + "output_layered.pdf", sourceFolder + "cmp_output_layered.pdf", destinationFolder, "diff"));
441453
}
442454

455+
@Test
456+
public void testReadAllLayersFromPage1() throws IOException, InterruptedException {
457+
PdfDocument pdfDoc = new PdfDocument(new PdfReader(sourceFolder + "input_layered.pdf"),
458+
CompareTool.createTestPdfWriter(destinationFolder + "output_layered_2.pdf"));
459+
460+
PdfCanvas canvas = new PdfCanvas(pdfDoc, 1);
461+
462+
//create layer on page
463+
PdfLayer newLayer = new PdfLayer("appended", pdfDoc);
464+
canvas.setFontAndSize(PdfFontFactory.createFont(StandardFonts.HELVETICA), 18);
465+
PdfLayerTestUtils.addTextInsideLayer(newLayer, canvas, "APPENDED CONTENT", 200, 600);
466+
467+
List<PdfLayer> layersFromCatalog = pdfDoc.getCatalog().getOCProperties(true).getLayers();
468+
Assertions.assertEquals(13, layersFromCatalog.size());
469+
PdfPage page = pdfDoc.getPage(1);
470+
Set<PdfLayer> layersFromPage = page.getPdfLayers();
471+
Assertions.assertEquals(11, layersFromPage.size());
472+
473+
pdfDoc.close();
474+
Assertions.assertNull(new CompareTool().compareByContent(destinationFolder + "output_layered_2.pdf", sourceFolder + "cmp_output_layered_2.pdf", destinationFolder, "diff"));
475+
}
476+
477+
@Test
478+
public void testReadAllLayersFromPage2() throws IOException, InterruptedException {
479+
PdfDocument pdfDoc = new PdfDocument(new PdfReader(sourceFolder + "input_layers_in_resources_xobject.pdf"));
480+
481+
List<PdfLayer> layersFromCatalog = pdfDoc.getCatalog().getOCProperties(true).getLayers();
482+
Assertions.assertEquals(16, layersFromCatalog.size());
483+
PdfPage page = pdfDoc.getPage(2);
484+
Set<PdfLayer> layersFromPage = page.getPdfLayers();
485+
//There is 8 ocgs nested under the resources xobject on 2nd page
486+
Assertions.assertEquals(8, layersFromPage.size());
487+
pdfDoc.close();
488+
}
489+
490+
@Test
491+
public void testReadAllLayersFromDocumentWithComplexOCG() throws IOException, InterruptedException {
492+
PdfDocument pdfDoc = new PdfDocument(new PdfReader(sourceFolder + "input_complex_layers.pdf"),
493+
CompareTool.createTestPdfWriter(destinationFolder + "output_complex_layers.pdf"));
494+
495+
List<PdfLayer> layersFromCatalog = pdfDoc.getCatalog().getOCProperties(true).getLayers();
496+
Assertions.assertEquals(12, layersFromCatalog.size());
497+
PdfPage page = pdfDoc.getPage(1);
498+
Set<PdfLayer> layersFromPage = page.getPdfLayers();
499+
Assertions.assertEquals(10, layersFromPage.size());
500+
pdfDoc.close();
501+
}
502+
503+
504+
//Read OCGs from different locations (annotations, content streams, xObjects) test block
505+
506+
@Test
507+
public void testReadOcgFromStreamProperties() throws IOException {
508+
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
509+
try (PdfDocument document = new PdfDocument(new PdfWriter(outputStream))) {
510+
PdfPage page = document.addNewPage();
511+
512+
PdfResources pdfResource = page.getResources();
513+
pdfResource.addProperties(new PdfLayer("name", document).getPdfObject());
514+
pdfResource.makeIndirect(document);
515+
516+
Set<PdfLayer> layersFromPage = page.getPdfLayers();
517+
Assertions.assertEquals(1, layersFromPage.size());
518+
}
519+
}
520+
}
521+
522+
@Test
523+
public void testReadOcgFromAnnotation() throws IOException {
524+
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
525+
try (PdfDocument fromDocument = new PdfDocument(new PdfWriter(outputStream))) {
526+
PdfPage page = fromDocument.addNewPage();
527+
PdfAnnotation annotation = new PdfTextAnnotation(new Rectangle(50, 10));
528+
annotation.setLayer(new PdfLayer("name", fromDocument));
529+
page.addAnnotation(annotation);
530+
531+
Set<PdfLayer> layersFromPage = page.getPdfLayers();
532+
Assertions.assertEquals(1, layersFromPage.size());
533+
}
534+
}
535+
}
536+
537+
@Test
538+
public void testReadOcgFromFlushedAnnotation() throws IOException {
539+
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
540+
try (PdfDocument fromDocument = new PdfDocument(new PdfWriter(outputStream))) {
541+
PdfPage page = fromDocument.addNewPage();
542+
PdfAnnotation annotation = new PdfTextAnnotation(new Rectangle(50, 10));
543+
annotation.setLayer(new PdfLayer("name", fromDocument));
544+
page.addAnnotation(annotation);
545+
annotation.flush();
546+
547+
Set<PdfLayer> layersFromPage = page.getPdfLayers();
548+
Assertions.assertEquals(1, layersFromPage.size());
549+
}
550+
}
551+
}
552+
553+
@Test
554+
public void testReadOcgFromApAnnotation() throws IOException {
555+
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
556+
try (PdfDocument fromDocument = new PdfDocument(new PdfWriter(outputStream))) {
557+
PdfPage page = fromDocument.addNewPage();
558+
559+
PdfAnnotation annotation = new PdfTextAnnotation(new Rectangle(50, 10));
560+
561+
PdfFormXObject formXObject = new PdfFormXObject(new Rectangle(50, 10));
562+
formXObject.setLayer(new PdfLayer("someName1", fromDocument));
563+
formXObject.makeIndirect(fromDocument);
564+
PdfDictionary nDict = new PdfDictionary();
565+
nDict.put(PdfName.ON, formXObject.getPdfObject());
566+
annotation.setAppearance(PdfName.N, nDict);
567+
568+
formXObject = new PdfFormXObject(new Rectangle(50, 10));
569+
formXObject.setLayer(new PdfLayer("someName2", fromDocument));
570+
PdfResources formResources = formXObject.getResources();
571+
formResources.addProperties(new PdfLayer("someName3", fromDocument).getPdfObject());
572+
formXObject.makeIndirect(fromDocument);
573+
PdfDictionary rDict = new PdfDictionary();
574+
rDict.put(PdfName.OFF, formXObject.getPdfObject());
575+
annotation.setAppearance(PdfName.R, rDict);
576+
577+
formXObject = new PdfFormXObject(new Rectangle(50, 10));
578+
formXObject.setLayer(new PdfLayer("someName4", fromDocument));
579+
formXObject.makeIndirect(fromDocument);
580+
annotation.setAppearance(PdfName.D, formXObject.getPdfObject());
581+
582+
page.addAnnotation(annotation);
583+
584+
Set<PdfLayer> layersFromPage = page.getPdfLayers();
585+
Assertions.assertEquals(4, layersFromPage.size());
586+
}
587+
}
588+
}
589+
443590
//TODO DEVSIX-8490 remove this test when implemented
444591
@Test
445592
public void addSecondParentlayerTest() throws IOException {

0 commit comments

Comments
 (0)