com.itextpdf.kernel.pdf.layer.PdfOCProperties Maven / Gradle / Ivy
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2024 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package com.itextpdf.kernel.pdf.layer;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.pdf.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* This class represents /OCProperties entry if pdf catalog and manages
* the layers of the pdf document.
*
* To be able to be wrapped with this {@link PdfObjectWrapper} the {@link PdfObject}
* must be indirect.
*/
public class PdfOCProperties extends PdfObjectWrapper {
static final String OC_CONFIG_NAME_PATTERN = "OCConfigName";
private List layers = new ArrayList<>();
/**
* Creates a new PdfOCProperties instance.
*
* @param document the document the optional content belongs to
*/
public PdfOCProperties(PdfDocument document) {
this((PdfDictionary) new PdfDictionary().makeIndirect(document));
}
/**
* Creates a new PdfOCProperties instance by the dictionary it represents,
* the dictionary must be an indirect object.
*
* @param ocPropertiesDict the dictionary of optional content properties, must have an indirect reference.
*/
public PdfOCProperties(PdfDictionary ocPropertiesDict) {
super(ocPropertiesDict);
ensureObjectIsAddedToDocument(ocPropertiesDict);
readLayersFromDictionary();
}
/**
* Use this method to set a collection of optional content groups
* whose states are intended to follow a "radio button" paradigm.
* That is, the state of at most one optional content group
* in the array should be ON at a time: if one group is turned
* ON, all others must be turned OFF.
*
* @param group the radio group
*/
public void addOCGRadioGroup(List group) {
PdfArray ar = new PdfArray();
for (PdfLayer layer : group) {
if (layer.getTitle() == null)
ar.add(layer.getPdfObject().getIndirectReference());
}
if (ar.size() != 0) {
PdfDictionary d = getPdfObject().getAsDictionary(PdfName.D);
if (d == null) {
d = new PdfDictionary();
getPdfObject().put(PdfName.D, d);
}
PdfArray radioButtonGroups = d.getAsArray(PdfName.RBGroups);
if (radioButtonGroups == null) {
radioButtonGroups = new PdfArray();
d.put(PdfName.RBGroups, radioButtonGroups);
d.setModified();
} else {
radioButtonGroups.setModified();
}
radioButtonGroups.add(ar);
}
}
/**
* Fills the underlying PdfDictionary object with the current layers and their settings.
* Note that it completely regenerates the dictionary, so your direct changes to the dictionary
* will not take any affect.
*
* @return the resultant dictionary
*/
public PdfObject fillDictionary() {
return this.fillDictionary(true);
}
/**
* Fills the underlying PdfDictionary object with the current layers and their settings.
* Note that it completely regenerates the dictionary, so your direct changes to the dictionary
* will not take any affect.
*
* @param removeNonDocumentOcgs the flag indicating whether it is necessary
* to delete OCGs not from the current document
* @return the resultant dictionary
*/
public PdfObject fillDictionary(boolean removeNonDocumentOcgs) {
PdfArray gr = new PdfArray();
for (PdfLayer layer : layers) {
if (layer.getTitle() == null)
gr.add(layer.getIndirectReference());
}
getPdfObject().put(PdfName.OCGs, gr);
PdfDictionary filledDDictionary = new PdfDictionary();
// Save radio groups,Name,BaseState,Intent,ListMode
PdfDictionary dDictionary = getPdfObject().getAsDictionary(PdfName.D);
if (dDictionary != null) {
PdfOCProperties.copyDDictionaryField(PdfName.RBGroups, dDictionary, filledDDictionary);
PdfOCProperties.copyDDictionaryField(PdfName.Name, dDictionary, filledDDictionary);
PdfOCProperties.copyDDictionaryField(PdfName.BaseState, dDictionary, filledDDictionary);
PdfOCProperties.copyDDictionaryField(PdfName.Intent, dDictionary, filledDDictionary);
PdfOCProperties.copyDDictionaryField(PdfName.ListMode, dDictionary, filledDDictionary);
}
if (filledDDictionary.get(PdfName.Name) == null) {
filledDDictionary.put(PdfName.Name, new PdfString(createUniqueName(), PdfEncodings.UNICODE_BIG));
}
getPdfObject().put(PdfName.D, filledDDictionary);
List docOrder = new ArrayList<>(layers);
for (int i = 0; i < docOrder.size(); i++) {
PdfLayer layer = docOrder.get(i);
if (layer.getParent() != null) {
docOrder.remove(layer);
i--;
}
}
PdfArray order = new PdfArray();
for (Object element : docOrder) {
PdfLayer layer = (PdfLayer) element;
getOCGOrder(order, layer);
}
filledDDictionary.put(PdfName.Order, order);
PdfArray off = new PdfArray();
for (Object element : layers) {
PdfLayer layer = (PdfLayer) element;
if (layer.getTitle() == null && !layer.isOn())
off.add(layer.getIndirectReference());
}
if (off.size() > 0) {
filledDDictionary.put(PdfName.OFF, off);
}
PdfArray locked = new PdfArray();
for (PdfLayer layer : layers) {
if (layer.getTitle() == null && layer.isLocked())
locked.add(layer.getIndirectReference());
}
if (locked.size() > 0) {
filledDDictionary.put(PdfName.Locked, locked);
}
addASEvent(PdfName.View, PdfName.Zoom);
addASEvent(PdfName.View, PdfName.View);
addASEvent(PdfName.Print, PdfName.Print);
addASEvent(PdfName.Export, PdfName.Export);
if (removeNonDocumentOcgs) {
this.removeNotRegisteredOcgs();
}
return getPdfObject();
}
/**
* Checks if optional content group default configuration dictionary field value matches
* the required value for this field, if one exists.
*
* @param field default configuration dictionary field.
* @param value value of that field.
*
* @return boolean indicating if field meets requirement.
*/
public static boolean checkDDictonaryFieldValue(PdfName field, PdfObject value) {
// dictionary D BaseState should have the value ON
if (PdfName.BaseState.equals(field) && !PdfName.ON.equals(value)) {
return false;
//for dictionary D Intent should have the value View
} else if (PdfName.Intent.equals(field) && !PdfName.View.equals(value)) {
return false;
}
return true;
}
@Override
public void flush() {
fillDictionary();
super.flush();
}
/**
* Gets the list of all the layers currently registered in the OCProperties.
* Note that this is just a new list and modifications to it will not affect anything.
*
* @return list of all the {@link PdfLayer layers} currently registered in the OCProperties
*/
public List getLayers() {
return new ArrayList<>(layers);
}
@Override
protected boolean isWrappedObjectMustBeIndirect() {
return true;
}
/**
* This method registers a new layer in the OCProperties.
*
* @param layer the new layer
* @throws IllegalArgumentException if layer param is null
*/
protected void registerLayer(PdfLayer layer) {
if (layer == null)
throw new IllegalArgumentException("layer argument is null");
layers.add(layer);
}
/**
* Gets the {@link PdfDocument} that owns that OCProperties.
*
* @return the {@link PdfDocument} that owns that OCProperties
*/
protected PdfDocument getDocument() {
return getPdfObject().getIndirectReference().getDocument();
}
/**
* Gets the order of the layers in which they will be displayed in the layer view panel,
* including nesting.
*/
private static void getOCGOrder(PdfArray order, PdfLayer layer) {
if (!layer.isOnPanel())
return;
if (layer.getTitle() == null)
order.add(layer.getPdfObject().getIndirectReference());
List children = layer.getChildren();
if (children == null)
return;
PdfArray kids = new PdfArray();
if (layer.getTitle() != null)
kids.add(new PdfString(layer.getTitle(), PdfEncodings.UNICODE_BIG));
for (PdfLayer child : children) {
getOCGOrder(kids, child);
}
if (kids.size() > 0)
order.add(kids);
}
private static void copyDDictionaryField(PdfName fieldToAdd, PdfDictionary fromDictionary, PdfDictionary toDictionary) {
PdfObject value = fromDictionary.get(fieldToAdd);
if(value != null) {
if (PdfOCProperties.checkDDictonaryFieldValue(fieldToAdd, value)) {
toDictionary.put(fieldToAdd, value);
} else {
Logger logger = LoggerFactory.getLogger(PdfOCProperties.class);
String warnText = MessageFormatUtil.format(KernelLogMessageConstant.INVALID_DDICTIONARY_FIELD_VALUE,
fieldToAdd, value);
logger.warn(warnText);
}
}
}
private void removeNotRegisteredOcgs() {
final PdfDictionary dDict = getPdfObject().getAsDictionary(PdfName.D);
final PdfDictionary ocProperties = this.getDocument().getCatalog().getPdfObject().getAsDictionary(PdfName.OCProperties);
final Set ocgsFromDocument = new HashSet<>();
if (ocProperties.getAsArray(PdfName.OCGs) != null) {
final PdfArray ocgs = ocProperties.getAsArray(PdfName.OCGs);
for (final PdfObject ocgObj : ocgs) {
if (ocgObj.isDictionary()) {
ocgsFromDocument.add(ocgObj.getIndirectReference());
}
}
}
// Remove from RBGroups OCGs not presented in the output document (in OCProperties/OCGs)
final PdfArray rbGroups = dDict.getAsArray(PdfName.RBGroups);
if (rbGroups != null) {
for (final PdfObject rbGroupObj : rbGroups) {
final PdfArray rbGroup = (PdfArray) rbGroupObj;
for (int i = rbGroup.size() - 1; i > -1; i--) {
if (!ocgsFromDocument.contains(rbGroup.get(i).getIndirectReference())) {
rbGroup.remove(i);
}
}
}
}
}
/**
* Populates the /AS entry in the /D dictionary.
*/
private void addASEvent(PdfName event, PdfName category) {
PdfArray arr = new PdfArray();
for (PdfLayer layer : layers) {
if (layer.getTitle() == null && !layer.getPdfObject().isFlushed()) {
PdfDictionary usage = layer.getPdfObject().getAsDictionary(PdfName.Usage);
if (usage != null && usage.get(category) != null)
arr.add(layer.getPdfObject().getIndirectReference());
}
}
if (arr.size() == 0)
return;
PdfDictionary d = getPdfObject().getAsDictionary(PdfName.D);
PdfArray arras = d.getAsArray(PdfName.AS);
if (arras == null) {
arras = new PdfArray();
d.put(PdfName.AS, arras);
}
PdfDictionary as = new PdfDictionary();
as.put(PdfName.Event, event);
PdfArray categoryArray = new PdfArray();
categoryArray.add(category);
as.put(PdfName.Category, categoryArray);
as.put(PdfName.OCGs, arr);
arras.add(as);
}
/**
* Reads the layers from the document to be able to modify them in the future.
*/
private void readLayersFromDictionary() {
PdfArray ocgs = getPdfObject().getAsArray(PdfName.OCGs);
if (ocgs == null || ocgs.isEmpty())
return;
Map layerMap = new TreeMap();
for (int ind = 0; ind < ocgs.size(); ind++) {
PdfLayer currentLayer = new PdfLayer((PdfDictionary) ocgs.getAsDictionary(ind).makeIndirect(getDocument()));
// We will set onPanel to true later for the objects present in /D->/Order entry.
currentLayer.onPanel = false;
layerMap.put(currentLayer.getIndirectReference(), currentLayer);
}
PdfDictionary d = getPdfObject().getAsDictionary(PdfName.D);
if (d != null && !d.isEmpty()) {
PdfArray off = d.getAsArray(PdfName.OFF);
if (off != null) {
for (int i = 0; i < off.size(); i++) {
PdfObject offLayer = off.get(i, false);
if (offLayer.isIndirectReference()) {
layerMap.get((PdfIndirectReference) offLayer).on = false;
} else {
layerMap.get(offLayer.getIndirectReference()).on = false;
}
}
}
PdfArray locked = d.getAsArray(PdfName.Locked);
if (locked != null) {
for (int i = 0; i < locked.size(); i++) {
PdfObject lockedLayer = locked.get(i, false);
if (lockedLayer.isIndirectReference()) {
layerMap.get((PdfIndirectReference) lockedLayer).locked = true;
} else {
layerMap.get(lockedLayer.getIndirectReference()).locked = true;
}
}
}
PdfArray orderArray = d.getAsArray(PdfName.Order);
if (orderArray != null && !orderArray.isEmpty())
readOrderFromDictionary(null, orderArray, layerMap);
}
// Add the layers which should not be displayed on the panel to the order list
for (PdfLayer layer : layerMap.values()) {
if (!layer.isOnPanel())
layers.add(layer);
}
}
/**
* Reads the /Order in the /D entry and initialized the parent-child hierarchy.
*/
private void readOrderFromDictionary(PdfLayer parent, PdfArray orderArray, Map layerMap) {
for (int i = 0; i < orderArray.size(); i++) {
PdfObject item = orderArray.get(i);
if (item.getType() == PdfObject.DICTIONARY) {
PdfLayer layer = layerMap.get(item.getIndirectReference());
if (layer != null) {
layers.add(layer);
layer.onPanel = true;
if (parent != null)
parent.addChild(layer);
if (i + 1 < orderArray.size() && orderArray.get(i + 1).getType() == PdfObject.ARRAY) {
final PdfArray nextArray = orderArray.getAsArray(i + 1);
if (nextArray.size() > 0 && nextArray.get(0).getType() != PdfObject.STRING) {
readOrderFromDictionary(layer, orderArray.getAsArray(i + 1), layerMap);
i++;
}
}
}
} else if (item.getType() == PdfObject.ARRAY) {
PdfArray subArray = (PdfArray) item;
if (subArray.isEmpty()) continue;
PdfObject firstObj = subArray.get(0);
if (firstObj.getType() == PdfObject.STRING) {
PdfLayer titleLayer = PdfLayer.createTitleSilent(((PdfString) firstObj).toUnicodeString(), getDocument());
titleLayer.onPanel = true;
layers.add(titleLayer);
if (parent != null)
parent.addChild(titleLayer);
readOrderFromDictionary(titleLayer, new PdfArray(subArray.subList(1, subArray.size())), layerMap);
} else {
readOrderFromDictionary(parent, subArray, layerMap);
}
}
}
}
private String createUniqueName() {
int uniqueID = 0;
Set usedNames = new HashSet<>();
PdfArray configs = getPdfObject().getAsArray(PdfName.Configs);
if (null != configs) {
for (int i = 0; i < configs.size(); i++) {
PdfDictionary alternateDictionary = configs.getAsDictionary(i);
if (null != alternateDictionary && alternateDictionary.containsKey(PdfName.Name)) {
usedNames.add(alternateDictionary.getAsString(PdfName.Name).toUnicodeString());
}
}
}
while (usedNames.contains(OC_CONFIG_NAME_PATTERN + uniqueID)) {
uniqueID++;
}
return OC_CONFIG_NAME_PATTERN + uniqueID;
}
}