com.itextpdf.pdfa.checker.PdfA4Checker 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.pdfa.checker;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.colors.IccProfile;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.PdfAConformanceLevel;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfCatalog;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.canvas.CanvasGraphicsState;
import com.itextpdf.kernel.pdf.colorspace.PdfCieBasedCs;
import com.itextpdf.kernel.pdf.colorspace.PdfColorSpace;
import com.itextpdf.kernel.pdf.colorspace.PdfSpecialCs;
import com.itextpdf.kernel.xmp.XMPConst;
import com.itextpdf.kernel.xmp.XMPException;
import com.itextpdf.kernel.xmp.XMPMeta;
import com.itextpdf.kernel.xmp.XMPMetaFactory;
import com.itextpdf.kernel.xmp.properties.XMPProperty;
import com.itextpdf.pdfa.PdfAXMPUtil;
import com.itextpdf.pdfa.exceptions.PdfAConformanceException;
import com.itextpdf.pdfa.exceptions.PdfaExceptionMessageConstant;
import com.itextpdf.pdfa.logs.PdfAConformanceLogMessageConstant;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* PdfA4Checker defines the requirements of the PDF/A-4 standard and contains a
* number of methods that override the implementations of its superclass
* {@link PdfA3Checker}.
*
* The specification implemented by this class is ISO 19005-4
*/
public class PdfA4Checker extends PdfA3Checker {
private static final String CALRGB_COLOR_SPACE = "CalRGB";
private static final Set forbiddenAnnotations4 = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(
PdfName._3D,
PdfName.RichMedia,
PdfName.FileAttachment,
PdfName.Sound,
PdfName.Screen,
PdfName.Movie)));
private static final Set forbiddenAnnotations4E = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(
PdfName.FileAttachment,
PdfName.Sound,
PdfName.Screen,
PdfName.Movie)));
private static final Set forbiddenAnnotations4F = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(
PdfName._3D,
PdfName.RichMedia,
PdfName.Sound,
PdfName.Screen,
PdfName.Movie)));
private static final Set apLessAnnotations = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(PdfName.Popup, PdfName.Link, PdfName.Projection)));
private static final Set allowedBlendModes4 = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(
PdfName.Normal,
PdfName.Multiply,
PdfName.Screen,
PdfName.Overlay,
PdfName.Darken,
PdfName.Lighten,
PdfName.ColorDodge,
PdfName.ColorBurn,
PdfName.HardLight,
PdfName.SoftLight,
PdfName.Difference,
PdfName.Exclusion,
PdfName.Hue,
PdfName.Saturation,
PdfName.Color,
PdfName.Luminosity)));
private static final String TRANSPARENCY_ERROR_MESSAGE =
PdfaExceptionMessageConstant.THE_DOCUMENT_AND_THE_PAGE_DO_NOT_CONTAIN_A_PDFA_OUTPUTINTENT_BUT_PAGE_CONTAINS_TRANSPARENCY_AND_DOES_NOT_CONTAIN_BLENDING_COLOR_SPACE;
private static final Logger LOGGER = LoggerFactory.getLogger(PdfAChecker.class);
private static final Set forbiddenActionsE = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(
PdfName.Launch,
PdfName.Sound,
PdfName.Movie,
PdfName.ResetForm,
PdfName.ImportData,
PdfName.JavaScript,
PdfName.Hide,
PdfName.Rendition,
PdfName.Trans
)));
private static final Set allowedEntriesInAAWhenNonWidget = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(
PdfName.E,
PdfName.X,
PdfName.D,
PdfName.U,
PdfName.Fo,
PdfName.Bl
)));
// Map pdfObject using CMYK - list of CMYK icc profile streams
private Map> iccBasedCmykObjects = new HashMap<>();
/**
* Creates a PdfA4Checker with the required conformance level
*
* @param conformanceLevel the required conformance level
*/
public PdfA4Checker(PdfAConformanceLevel conformanceLevel) {
super(conformanceLevel);
}
/**
* {@inheritDoc}
*/
@Override
public void checkColorSpace(PdfColorSpace colorSpace, PdfObject pdfObject, PdfDictionary currentColorSpaces,
boolean checkAlternate, Boolean fill) {
if (colorSpace instanceof PdfCieBasedCs.IccBased) {
// 6.2.4.2: An ICCBased colour space shall not be used where the profile is a CMYK destination profile and is
// identical to that in the current PDF/A OutputIntent or the current transparency blending colorspace.
PdfStream iccStream = ((PdfArray) colorSpace.getPdfObject()).getAsStream(1);
byte[] iccBytes = iccStream.getBytes();
// If not CMYK - we don't care
if (ICC_COLOR_SPACE_CMYK.equals(IccProfile.getIccColorSpaceName(iccBytes))) {
if (!iccBasedCmykObjects.containsKey(pdfObject)) {
iccBasedCmykObjects.put(pdfObject, new ArrayList<>());
}
iccBasedCmykObjects.get(pdfObject).add(iccStream);
}
}
super.checkColorSpace(colorSpace, pdfObject, currentColorSpaces,checkAlternate, fill);
}
/**
* {@inheritDoc}
*/
@Override
protected void checkPageColorsUsages(PdfDictionary pageDict, PdfDictionary pageResources) {
// Get page pdf/a output intent output profile
PdfStream pageDestOutputProfile = null;
PdfArray outputIntents = pageDict.getAsArray(PdfName.OutputIntents);
if (outputIntents != null) {
PdfDictionary pdfAPageOutputIntent = getPdfAOutputIntent(outputIntents);
if (pdfAPageOutputIntent != null) {
pageDestOutputProfile = pdfAPageOutputIntent.getAsStream(PdfName.DestOutputProfile);
}
}
if (pageDestOutputProfile == null) {
pageDestOutputProfile = pdfAOutputIntentDestProfile;
}
// Page blending colorspace should be taken into account while checking objects using device dependent colors
PdfColorSpace pageTransparencyBlendingCS = getDeviceIndependentTransparencyBlendingCSIfRbgOrCmykBased(pageDict);
// We don't know on which pages these objects are so that we have to go inside anyway
if (!rgbUsedObjects.isEmpty() || !cmykUsedObjects.isEmpty() || !grayUsedObjects.isEmpty() ||
!iccBasedCmykObjects.isEmpty()) {
checkPageContentsForColorUsages(pageDict, pageDestOutputProfile, pageTransparencyBlendingCS);
checkAnnotationsForColorUsages(pageDict.getAsArray(PdfName.Annots), pageDestOutputProfile,
pageTransparencyBlendingCS);
checkResourcesForColorUsages(pageResources, pageDestOutputProfile, pageTransparencyBlendingCS);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkTrailer(PdfDictionary trailer) {
super.checkTrailer(trailer);
if (trailer.get(PdfName.Info) != null) {
PdfDictionary info = trailer.getAsDictionary(PdfName.Info);
if (info.size() != 1 || info.get(PdfName.ModDate) == null) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.DOCUMENT_INFO_DICTIONARY_SHALL_ONLY_CONTAIN_MOD_DATE);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkCatalog(PdfCatalog catalog) {
if ('2' != catalog.getDocument().getPdfVersion().toString().charAt(4)) {
throw new PdfAConformanceException(
MessageFormatUtil.format(
PdfaExceptionMessageConstant.THE_FILE_HEADER_SHALL_CONTAIN_RIGHT_PDF_VERSION, "2"));
}
PdfDictionary trailer = catalog.getDocument().getTrailer();
if (trailer.get(PdfName.Info) != null) {
if (catalog.getPdfObject().get(PdfName.PieceInfo) == null) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.DOCUMENT_SHALL_NOT_CONTAIN_INFO_UNLESS_THERE_IS_PIECE_INFO);
}
}
if ("F".equals(conformanceLevel.getConformance())) {
if (!catalog.nameTreeContainsKey(PdfName.EmbeddedFiles)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.NAME_DICTIONARY_SHALL_CONTAIN_EMBEDDED_FILES_KEY);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkPageObject(PdfDictionary pageDict, PdfDictionary pageResources) {
super.checkPageObject(pageDict, pageResources);
PdfStream xmpMeta = pageDict.getAsStream(PdfName.Metadata);
if (xmpMeta != null && !PdfAXMPUtil.isUtf8(xmpMeta.getBytes())) {
throw new PdfAConformanceException(PdfaExceptionMessageConstant.INVALID_XMP_METADATA_ENCODING);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkCatalogValidEntries(PdfDictionary catalogDict) {
super.checkCatalogValidEntries(catalogDict);
PdfString version = catalogDict.getAsString(PdfName.Version);
if (version != null && (version.toString().charAt(0) != '2'
|| version.toString().charAt(1) != '.' || !Character.isDigit(version.toString().charAt(2)))) {
throw new PdfAConformanceException(
MessageFormatUtil.format(
PdfaExceptionMessageConstant.THE_CATALOG_VERSION_SHALL_CONTAIN_RIGHT_PDF_VERSION, "2"));
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkFileSpec(PdfDictionary fileSpec) {
if (fileSpec.getAsName(PdfName.AFRelationship) == null) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_AFRELATIONSHIP_KEY);
}
if (!fileSpec.containsKey(PdfName.F) || !fileSpec.containsKey(PdfName.UF)) {
throw new PdfAConformanceException(
PdfAConformanceException.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_F_KEY_AND_UF_KEY);
}
if (!fileSpec.containsKey(PdfName.Desc)) {
LOGGER.warn(PdfAConformanceLogMessageConstant.FILE_SPECIFICATION_DICTIONARY_SHOULD_CONTAIN_DESC_KEY);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkPageTransparency(PdfDictionary pageDict, PdfDictionary pageResources) {
// Get page pdf/a output intent
PdfDictionary pdfAPageOutputIntent = null;
PdfArray outputIntents = pageDict.getAsArray(PdfName.OutputIntents);
if (outputIntents != null) {
pdfAPageOutputIntent = getPdfAOutputIntent(outputIntents);
}
if (pdfAOutputIntentColorSpace == null && pdfAPageOutputIntent == null
&& !transparencyObjects.isEmpty()
&& (pageDict.getAsDictionary(PdfName.Group) == null
|| pageDict.getAsDictionary(PdfName.Group).get(PdfName.CS) == null)) {
checkContentsForTransparency(pageDict);
checkAnnotationsForTransparency(pageDict.getAsArray(PdfName.Annots));
checkResourcesForTransparency(pageResources, new HashSet());
}
}
/**
* Check the conformity of the AA dictionary on catalog level.
*
* @param dict the catalog dictionary
*/
@Override
protected void checkCatalogAAConformance(PdfDictionary dict) {
final PdfDictionary aa = dict.getAsDictionary(PdfName.AA);
if (aa != null && hasAAIllegalEntries(aa)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.CATALOG_AA_DICTIONARY_SHALL_CONTAIN_ONLY_ALLOWED_KEYS);
}
}
/**
* Check the conformity of the AA dictionary on catalog level.
*
* @param dict the catalog dictionary
*/
@Override
protected void checkPageAAConformance(PdfDictionary dict) {
final PdfDictionary aa = dict.getAsDictionary(PdfName.AA);
if (aa != null && hasAAIllegalEntries(aa)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.PAGE_AA_DICTIONARY_SHALL_CONTAIN_ONLY_ALLOWED_KEYS);
}
}
//There are no limits for numbers in pdf-a/4
/**
* {@inheritDoc}
*/
@Override
protected void checkPdfNumber(PdfNumber number) {
}
//There is no limit for canvas stack in pdf-a/4
/**
* {@inheritDoc}
*/
@Override
public void checkCanvasStack(char stackOperation) {
}
/**
* {@inheritDoc}
*/
@Override
public void checkSignatureType(boolean isCAdES) {
if (!isCAdES) {
throw new PdfAConformanceException(PdfaExceptionMessageConstant.SIGNATURE_SHALL_CONFORM_TO_ONE_OF_THE_PADES_PROFILE);
}
}
//There is no limit for String length in pdf-a/4
/**
* {@inheritDoc}
*/
@Override
protected int getMaxStringLength() {
return Integer.MAX_VALUE;
}
//There is no limit for DeviceN components count in pdf-a/4
/**
* {@inheritDoc}
*/
@Override
protected void checkNumberOfDeviceNComponents(PdfSpecialCs.DeviceN deviceN) {
}
@Override
public void checkExtGState(CanvasGraphicsState extGState, PdfStream contentStream) {
super.checkExtGState(extGState, contentStream);
if (extGState.getHalftone() instanceof PdfDictionary) {
PdfDictionary halftoneDict = (PdfDictionary) extGState.getHalftone();
if (halftoneDict.containsKey(PdfName.TransferFunction)) {
throw new PdfAConformanceException(PdfaExceptionMessageConstant.ALL_HALFTONES_CONTAINING_TRANSFER_FUNCTION_SHALL_HAVE_HALFTONETYPE_5);
}
int halftoneType = halftoneDict.getAsInt(PdfName.HalftoneType).intValue();
if (halftoneType == 5) {
for (Map.Entry entry : halftoneDict.entrySet()) {
//see ISO_32000_2;2020 table 132
if (PdfName.Type.equals(entry.getKey()) || PdfName.HalftoneType.equals(entry.getKey()) || PdfName.HalftoneName.equals(entry.getKey())) {
continue;
}
if (entry.getValue() instanceof PdfDictionary && isCMYKColorant(entry.getKey()) && entry.getValue() instanceof PdfDictionary && ((PdfDictionary)entry.getValue()).containsKey(PdfName.TransferFunction)) {
throw new PdfAConformanceException(PdfaExceptionMessageConstant.ALL_HALFTONES_CONTAINING_TRANSFER_FUNCTION_SHALL_HAVE_HALFTONETYPE_5);
}
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkFormXObject(PdfStream form, PdfStream contentStream) {
if (isAlreadyChecked(form)) {
return;
}
if (form.containsKey(PdfName.OPI)) {
throw new PdfAConformanceException(PdfaExceptionMessageConstant.A_FORM_XOBJECT_DICTIONARY_SHALL_NOT_CONTAIN_OPI_KEY);
}
if (form.containsKey(PdfName.Ref)) {
throw new PdfAConformanceException(PdfaExceptionMessageConstant.A_FORM_XOBJECT_DICTIONARY_SHALL_NOT_CONTAIN_REF_KEY);
}
checkTransparencyGroup(form, contentStream);
checkResources(form.getAsDictionary(PdfName.Resources), form);
checkContentStream(form);
}
/**
* {@inheritDoc}
*/
@Override
protected void checkAnnotation(PdfDictionary annotDic) {
super.checkAnnotation(annotDic);
// Extra check for blending mode
PdfName blendMode = annotDic.getAsName(PdfName.BM);
if (blendMode != null && !allowedBlendModes4.contains(blendMode)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.ONLY_STANDARD_BLEND_MODES_SHALL_BE_USED_FOR_THE_VALUE_OF_THE_BM_KEY_IN_A_GRAPHIC_STATE_AND_ANNOTATION_DICTIONARY);
}
// And then treat the annotation as an object with transparency
if (blendMode != null && !PdfName.Normal.equals(blendMode)) {
transparencyObjects.add(annotDic);
}
}
/**
* {@inheritDoc}
*/
@Override
protected Set getForbiddenAnnotations() {
if ("E".equals(conformanceLevel.getConformance())) {
return forbiddenAnnotations4E;
} else if ("F".equals(conformanceLevel.getConformance())) {
return forbiddenAnnotations4F;
}
return forbiddenAnnotations4;
}
/**
* {@inheritDoc}
*/
@Override
protected Set getAppearanceLessAnnotations() {
return apLessAnnotations;
}
/**
* Check the conformity of the AA dictionary on widget level.
*
* @param dict the widget dictionary
*/
protected void checkWidgetAAConformance(PdfDictionary dict) {
if (!PdfName.Widget.equals(dict.getAsName(PdfName.Subtype)) && dict.containsKey(PdfName.AA)) {
final PdfObject additionalActions = dict.get(PdfName.AA);
if (additionalActions.isDictionary() && hasAAIllegalEntries((PdfDictionary) additionalActions)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.ANNOTATION_AA_DICTIONARY_SHALL_CONTAIN_ONLY_ALLOWED_KEYS);
}
}
}
/**
* @param catalog the catalog {@link PdfDictionary} to check
*/
@Override
protected void checkMetaData(PdfDictionary catalog) {
super.checkMetaData(catalog);
try {
final PdfStream xmpMetadata = catalog.getAsStream(PdfName.Metadata);
byte[] bytes = xmpMetadata.getBytes();
isValidEncoding(bytes);
checkPacketHeader(bytes);
final XMPMeta meta = XMPMetaFactory.parse(new ByteArrayInputStream(bytes));
checkVersionIdentification(meta);
checkFileProvenanceSpec(meta);
} catch (XMPException ex) {
throw new PdfException(ex);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkOutputIntents(PdfDictionary catalog) {
super.checkOutputIntents(catalog);
final PdfArray outputIntents = catalog.getAsArray(PdfName.OutputIntents);
if (outputIntents == null) {
return;
}
for (int i = 0; i < outputIntents.size(); ++i) {
final PdfDictionary outputIntent = outputIntents.getAsDictionary(i);
if (outputIntent.containsKey(new PdfName("DestOutputProfileRef"))) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.OUTPUTINTENT_SHALL_NOT_CONTAIN_DESTOUTPUTPROFILEREF_KEY);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void checkAnnotationAgainstActions(PdfDictionary annotDic) {
if (PdfName.Widget.equals(annotDic.getAsName(PdfName.Subtype)) && annotDic.containsKey(PdfName.A)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.WIDGET_ANNOTATION_DICTIONARY_OR_FIELD_DICTIONARY_SHALL_NOT_INCLUDE_A_ENTRY);
}
checkWidgetAAConformance(annotDic);
}
private static boolean hasAAIllegalEntries(PdfDictionary aa) {
for (final PdfName key : aa.keySet()) {
if (!allowedEntriesInAAWhenNonWidget.contains(key)) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
protected Set getForbiddenActions() {
if ("E".equals(conformanceLevel.getConformance())) {
return forbiddenActionsE;
}
return super.getForbiddenActions();
}
/**
* {@inheritDoc}
*/
@Override
protected void checkContentConfigurationDictAgainstAsKey(PdfDictionary config) {
// Do nothing because in PDF/A-4 AS key may appear in any optional content configuration dictionary.
}
/**
* {@inheritDoc}
*/
@Override
protected String getTransparencyErrorMessage() {
return TRANSPARENCY_ERROR_MESSAGE;
}
/**
* {@inheritDoc}
*/
@Override
protected void checkBlendMode(PdfName blendMode) {
if (!allowedBlendModes4.contains(blendMode)) {
throw new PdfAConformanceException(
PdfAConformanceException.ONLY_STANDARD_BLEND_MODES_SHALL_BE_USED_FOR_THE_VALUE_OF_THE_BM_KEY_IN_AN_EXTENDED_GRAPHIC_STATE_DICTIONARY);
}
}
/**
* {@inheritDoc}
*/
protected int getMaxNameLength() {
return Integer.MAX_VALUE;
}
private static void isValidEncoding(byte[] data) {
if (!PdfAXMPUtil.isUtf8(data)) {
throw new PdfAConformanceException(PdfaExceptionMessageConstant.INVALID_XMP_METADATA_ENCODING);
}
}
private static boolean isValidXmpConformance(String value) {
if (value == null) {
return false;
}
if (value.length() != 1) {
return false;
}
return "F".equals(value) || "E".equals(value);
}
private static boolean isValidXmpRevision(String value) {
if (value == null) {
return false;
}
if (value.length() != 4) {
return false;
}
for (final char c : value.toCharArray()) {
if (!Character.isDigit(c)) {
return false;
}
}
return true;
}
private void checkPacketHeader(byte[] meta) {
if (meta == null) {
return;
}
final String metAsStr = new String(meta);
final String regex = "<\\?xpacket.*encoding|bytes.*\\?>";
final Pattern pattern = Pattern.compile(regex);
if (pattern.matcher(metAsStr).find()) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant
.XMP_METADATA_HEADER_PACKET_MAY_NOT_CONTAIN_BYTES_OR_ENCODING_ATTRIBUTE);
}
}
private void checkFileProvenanceSpec(XMPMeta meta) {
try {
XMPProperty history = meta.getProperty(XMPConst.NS_XMP_MM, XMPConst.HISTORY);
if (history == null) {
return;
}
if (!history.getOptions().isArray()) {
return;
}
final int amountOfEntries = meta.countArrayItems(XMPConst.NS_XMP_MM, XMPConst.HISTORY);
for (int i = 0; i < amountOfEntries; i++) {
int nameSpaceIndex = i + 1;
if (!meta.doesPropertyExist(XMPConst.NS_XMP_MM,
XMPConst.HISTORY + "[" + nameSpaceIndex + "]/stEvt:action")) {
throw new PdfAConformanceException(MessageFormatUtil.format(
PdfaExceptionMessageConstant.XMP_METADATA_HISTORY_ENTRY_SHALL_CONTAIN_KEY,
"stEvt:action"));
}
if (!meta.doesPropertyExist(XMPConst.NS_XMP_MM,
XMPConst.HISTORY + "[" + nameSpaceIndex + "]/stEvt:when")) {
throw new PdfAConformanceException(MessageFormatUtil.format(
PdfaExceptionMessageConstant.XMP_METADATA_HISTORY_ENTRY_SHALL_CONTAIN_KEY,
"stEvt:when"));
}
}
} catch (XMPException e) {
throw new PdfException(e);
}
}
private void checkVersionIdentification(XMPMeta meta) {
try {
XMPProperty prop = meta.getProperty(XMPConst.NS_PDFA_ID, XMPConst.PART);
if (prop == null || !getConformanceLevel().getPart().equals(prop.getValue())) {
throw new PdfAConformanceException(MessageFormatUtil.format(
PdfaExceptionMessageConstant.XMP_METADATA_HEADER_SHALL_CONTAIN_VERSION_IDENTIFIER_PART,
getConformanceLevel().getPart()));
}
} catch (XMPException e) {
throw new PdfAConformanceException(MessageFormatUtil.format(
PdfaExceptionMessageConstant.XMP_METADATA_HEADER_SHALL_CONTAIN_VERSION_IDENTIFIER_PART,
getConformanceLevel().getPart()));
}
try {
XMPProperty prop = meta.getProperty(XMPConst.NS_PDFA_ID, XMPConst.REV);
if (prop == null || !isValidXmpRevision(prop.getValue())) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.XMP_METADATA_HEADER_SHALL_CONTAIN_VERSION_IDENTIFIER_REV);
}
} catch (XMPException e) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.XMP_METADATA_HEADER_SHALL_CONTAIN_VERSION_IDENTIFIER_REV);
}
try {
XMPProperty prop = meta.getProperty(XMPConst.NS_PDFA_ID, XMPConst.CONFORMANCE);
if (prop != null && !isValidXmpConformance(prop.getValue())) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.XMP_METADATA_HEADER_SHALL_CONTAIN_VERSION_IDENTIFIER_CONFORMANCE);
}
} catch (XMPException e) {
// ignored because it is not required
}
}
private boolean isCMYKColorant(PdfName colourant) {
return PdfName.Cyan.equals(colourant) || PdfName.Magenta.equals(colourant)
|| PdfName.Yellow.equals(colourant) || PdfName.Black.equals(colourant);
}
private void checkPageContentsForColorUsages(PdfDictionary pageDict, PdfStream pageIntentProfile,
PdfColorSpace pageTransparencyBlendingCS) {
PdfStream contentStream = pageDict.getAsStream(PdfName.Contents);
if (contentStream != null) {
checkContentForColorUsages(contentStream, pageIntentProfile, pageTransparencyBlendingCS);
} else {
PdfArray contentSteamArray = pageDict.getAsArray(PdfName.Contents);
if (contentSteamArray != null) {
for (int i = 0; i < contentSteamArray.size(); i++) {
checkContentForColorUsages(contentSteamArray.get(i), pageIntentProfile, pageTransparencyBlendingCS);
}
}
}
}
private void checkAnnotationsForColorUsages(PdfArray annotations, PdfStream pageIntentProfile,
PdfColorSpace pageTransparencyBlendingCS) {
if (annotations == null) {
return;
}
for (int i = 0; i < annotations.size(); ++i) {
PdfDictionary annot = annotations.getAsDictionary(i);
PdfDictionary ap = annot.getAsDictionary(PdfName.AP);
if (ap != null) {
checkAppearanceStreamForColorUsages(ap, pageIntentProfile, pageTransparencyBlendingCS);
}
}
}
private void checkAppearanceStreamForColorUsages(PdfDictionary ap, PdfStream pageIntentProfile,
PdfColorSpace pageTransparencyBlendingCS) {
checkContentForColorUsages(ap, pageIntentProfile, pageTransparencyBlendingCS);
for (final PdfObject val : ap.values()) {
checkContentForColorUsages(val, pageIntentProfile, pageTransparencyBlendingCS);
if (val.isDictionary()) {
checkAppearanceStreamForColorUsages((PdfDictionary) val, pageIntentProfile, pageTransparencyBlendingCS);
} else if (val.isStream()) {
checkObjectWithResourcesForColorUsages(val, pageIntentProfile, pageTransparencyBlendingCS);
}
}
}
private void checkObjectWithResourcesForColorUsages(PdfObject objectWithResources, PdfStream pageIntentProfile,
PdfColorSpace pageTransparencyBlendingCS) {
checkContentForColorUsages(objectWithResources, pageIntentProfile, pageTransparencyBlendingCS);
if (objectWithResources instanceof PdfDictionary) {
checkResourcesForColorUsages(((PdfDictionary) objectWithResources).getAsDictionary(PdfName.Resources),
pageIntentProfile, pageTransparencyBlendingCS);
}
}
private void checkResourcesForColorUsages(PdfDictionary resources, PdfStream pageIntentProfile,
PdfColorSpace pageTransparencyBlendingCS) {
if (resources != null) {
checkSingleResourceTypeForColorUsages(resources.getAsDictionary(PdfName.XObject), pageIntentProfile,
pageTransparencyBlendingCS);
checkSingleResourceTypeForColorUsages(resources.getAsDictionary(PdfName.Pattern), pageIntentProfile,
pageTransparencyBlendingCS);
}
}
private void checkSingleResourceTypeForColorUsages(PdfDictionary singleResourceDict, PdfStream pageIntentProfile,
PdfColorSpace pageTransparencyBlendingCS) {
if (singleResourceDict != null) {
for (PdfObject resource : singleResourceDict.values()) {
checkObjectWithResourcesForColorUsages(resource, pageIntentProfile, pageTransparencyBlendingCS);
}
}
}
private void checkContentForColorUsages(PdfObject pdfObject, PdfStream pageIntentProfile,
PdfColorSpace pageTransparencyBlendingCS) {
String pageIntentCSType = pageIntentProfile == null ? null :
IccProfile.getIccColorSpaceName(pageIntentProfile.getBytes());
PdfColorSpace currentTransparencyBlendingCS = pdfObject instanceof PdfDictionary ?
getDeviceIndependentTransparencyBlendingCSIfRbgOrCmykBased((PdfDictionary)pdfObject) : null;
// Step 1 - 6.2.4.3: check if device dependent color in the object is allowed
// Current output intent, page blending colorspace and object blending colorspace should be taken into account
// Step 1.1 - check if any excuse exists
if (pageIntentCSType == null && pageTransparencyBlendingCS == null && currentTransparencyBlendingCS == null) {
if (rgbUsedObjects.contains(pdfObject)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.DEVICERGB_SHALL_ONLY_BE_USED_IF_CURRENT_RGB_PDFA_OUTPUT_INTENT_OR_DEFAULTRGB_IN_USAGE_CONTEXT);
} else if (cmykUsedObjects.contains(pdfObject)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.DEVICECMYK_SHALL_ONLY_BE_USED_IF_CURRENT_CMYK_PDFA_OUTPUT_INTENT_OR_DEFAULTCMYK_IN_USAGE_CONTEXT);
}
}
// pageTransparencyBlendingCS currentTransparencyBlendingCS don't help for DeviceGray
if (grayUsedObjects.contains(pdfObject) && pageIntentCSType == null) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.DEVICEGRAY_SHALL_ONLY_BE_USED_IF_CURRENT_PDFA_OUTPUT_INTENT_OR_DEFAULTGRAY_IN_USAGE_CONTEXT);
}
String pageTransparencyBlendingCSType = getColorspaceTypeIfIccBasedOrCalRgb(pageTransparencyBlendingCS);
String currentTransparencyBlendingCSType = getColorspaceTypeIfIccBasedOrCalRgb(currentTransparencyBlendingCS);
// Step 1.2 - check for RGB
if (rgbUsedObjects.contains(pdfObject) && !ICC_COLOR_SPACE_RGB.equals(pageIntentCSType) &&
!ICC_COLOR_SPACE_RGB.equals(pageTransparencyBlendingCSType) &&
!ICC_COLOR_SPACE_RGB.equals(currentTransparencyBlendingCSType) &&
!CALRGB_COLOR_SPACE.equals(pageTransparencyBlendingCSType) &&
!CALRGB_COLOR_SPACE.equals(currentTransparencyBlendingCSType)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.DEVICERGB_SHALL_ONLY_BE_USED_IF_CURRENT_RGB_PDFA_OUTPUT_INTENT_OR_DEFAULTRGB_IN_USAGE_CONTEXT);
}
// Step 1.3 - check for CMYK
if (cmykUsedObjects.contains(pdfObject) && !ICC_COLOR_SPACE_CMYK.equals(pageIntentCSType) &&
!ICC_COLOR_SPACE_CMYK.equals(pageTransparencyBlendingCSType) &&
!ICC_COLOR_SPACE_CMYK.equals(currentTransparencyBlendingCSType)) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.DEVICECMYK_SHALL_ONLY_BE_USED_IF_CURRENT_CMYK_PDFA_OUTPUT_INTENT_OR_DEFAULTCMYK_IN_USAGE_CONTEXT);
}
// Step 2 - 6.2.4.2: An ICCBased colour space shall not be used where the profile is a CMYK destination profile and is
// identical to that in the current PDF/A OutputIntent or the current transparency blending colorspace.
List currentICCBasedProfiles = iccBasedCmykObjects.get(pdfObject);
if (currentICCBasedProfiles == null) {
return;
}
for (PdfStream currentICCBasedProfile : currentICCBasedProfiles) {
throwIfIdenticalProfiles(currentICCBasedProfile, pageIntentProfile);
if (ICC_COLOR_SPACE_CMYK.equals(currentTransparencyBlendingCSType)) {
PdfStream iccStream = ((PdfArray) currentTransparencyBlendingCS.getPdfObject()).getAsStream(1);
throwIfIdenticalProfiles(currentICCBasedProfile, iccStream);
}
if (ICC_COLOR_SPACE_CMYK.equals(pageTransparencyBlendingCSType)) {
PdfStream iccStream = ((PdfArray) pageTransparencyBlendingCS.getPdfObject()).getAsStream(1);
throwIfIdenticalProfiles(currentICCBasedProfile, iccStream);
}
}
}
private static void throwIfIdenticalProfiles(PdfStream iccBasedProfile1, PdfStream iccBasedProfile2) {
if (iccBasedProfile1 != null && iccBasedProfile2 != null &&
(iccBasedProfile1.equals(iccBasedProfile2) ||
Arrays.equals(iccBasedProfile1.getBytes(), iccBasedProfile2.getBytes()))) {
throw new PdfAConformanceException(
PdfaExceptionMessageConstant.ICCBASED_COLOUR_SPACE_SHALL_NOT_BE_USED_IF_IT_IS_CMYK_AND_IS_IDENTICAL_TO_CURRENT_PROFILE);
}
}
private static String getColorspaceTypeIfIccBasedOrCalRgb(PdfColorSpace colorspace) {
if (colorspace instanceof PdfCieBasedCs.CalRgb) {
return CALRGB_COLOR_SPACE;
}
if (colorspace instanceof PdfCieBasedCs.IccBased) {
// 6.2.4.2: An ICCBased colour space shall not be used where the profile is a CMYK destination profile and is
// identical to that in the current PDF/A OutputIntent or the current transparency blending colorspace.
PdfStream iccStream = ((PdfArray) colorspace.getPdfObject()).getAsStream(1);
return IccProfile.getIccColorSpaceName(iccStream.getBytes());
}
return null;
}
private static PdfColorSpace getDeviceIndependentTransparencyBlendingCSIfRbgOrCmykBased(PdfDictionary pageDict) {
if (!isContainsTransparencyGroup(pageDict)) {
return null;
}
PdfObject cs = pageDict.getAsDictionary(PdfName.Group).get(PdfName.CS);
if (cs == null) {
return null;
}
PdfColorSpace transparencyBlendingCS = PdfColorSpace.makeColorSpace(cs);
if (transparencyBlendingCS instanceof PdfCieBasedCs.CalRgb ||
transparencyBlendingCS instanceof PdfCieBasedCs.IccBased) {
// Do not take others into account
return transparencyBlendingCS;
}
return null;
}
}