/******************************************************************************* * Copyright 2010 Atos Worldline SAS * * Licensed by Atos Worldline SAS under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Atos Worldline SAS licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ /** * */ package net.padaf.preflight.helpers; import java.awt.color.ICC_Profile; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import net.padaf.preflight.DocumentHandler; import net.padaf.preflight.ValidationException; import net.padaf.preflight.ValidatorConfig; import net.padaf.preflight.ValidationResult.ValidationError; import net.padaf.preflight.actions.AbstractActionManager; import net.padaf.preflight.graphics.ICCProfileWrapper; import net.padaf.preflight.utils.COSUtils; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSObject; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.persistence.util.COSObjectKey; /** * This helper validates the PDF file catalog */ public class CatalogValidationHelper extends AbstractValidationHelper { public CatalogValidationHelper(ValidatorConfig cfg) throws ValidationException { super(cfg); } /* * (non-Javadoc) * * @see * net.awl.edoc.pdfa.validation.helpers.AbstractValidationHelper#innerValidate * (net.awl.edoc.pdfa.validation.DocumentHandler) */ @Override public List<ValidationError> innerValidate(DocumentHandler handler) throws ValidationException { List<ValidationError> result = new ArrayList<ValidationError>(0); PDDocument pdfbox = handler.getDocument(); PDDocumentCatalog catalog = pdfbox.getDocumentCatalog(); if (catalog != null) { validateActions(handler, catalog, result); validateLang(handler, catalog, result); validateNames(handler, catalog, result); validateOCProperties(handler, catalog, result); } else { throw new ValidationException( "There are no Catalog entry in the Document."); } // ---- Check OutputIntent to know the ICC Profile result.addAll(validateOutputIntent(handler)); return result; } /** * This method validates if OpenAction entry contains forbidden action type. * It checks too if an Additional Action is present. * * @param handler * @param catalog * @param result * @throws ValidationException */ protected void validateActions(DocumentHandler handler, PDDocumentCatalog catalog, List<ValidationError> result) throws ValidationException { // ---- get OpenAction and Additional Action if these entries are present List<AbstractActionManager> lActions = this.actionFact.getActions(catalog .getCOSDictionary(), handler.getDocument().getDocument()); for (AbstractActionManager action : lActions) { if (!action.valid(result)) { return; } } } /** * The Lang element is optional but it is recommended. This method check the * Syntax of the Lang if this entry is present. * * @param handler * @param catalog * @param result * @throws ValidationException */ protected void validateLang(DocumentHandler handler, PDDocumentCatalog catalog, List<ValidationError> result) throws ValidationException { String lang = catalog.getLanguage(); if (lang != null && !lang.matches("[A-Za-z]{1,8}(-[A-Za-z]{1,8})*")) { result.add(new ValidationError(ERROR_SYNTAX_LANG_NOT_RFC1766)); } } /** * A Catalog shall not contain the EmbeddedFiles entry. * * @param handler * @param catalog * @param result * @throws ValidationException */ protected void validateNames(DocumentHandler handler, PDDocumentCatalog catalog, List<ValidationError> result) throws ValidationException { PDDocumentNameDictionary names = catalog.getNames(); if (names != null) { PDEmbeddedFilesNameTreeNode efs = names.getEmbeddedFiles(); if (efs != null) { result.add(new ValidationError( ERROR_SYNTAX_TRAILER_CATALOG_EMBEDDEDFILES,"EmbeddedFile entry is present in the Names dictionary")); } } } /** * A Catalog shall not contain the OCPProperties (Optional Content Properties) * entry. * * @param handler * @param catalog * @param result * @throws ValidationException */ protected void validateOCProperties(DocumentHandler handler, PDDocumentCatalog catalog, List<ValidationError> result) throws ValidationException { COSBase ocp = catalog.getCOSDictionary().getItem( COSName.getPDFName(DOCUMENT_DICTIONARY_KEY_OPTIONAL_CONTENTS)); if (ocp != null) { result .add(new ValidationError(ERROR_SYNTAX_TRAILER_CATALOG_OCPROPERTIES, "A Catalog shall not contain the OCPProperties entry.")); } } /** * This method checks the content of each OutputIntent. The S entry must * contain GTS_PDFA1. The DestOuputProfile must contain a valid ICC Profile * Stream. * * If there are more than one OutputIntent, they have to use the same ICC * Profile. * * This method returns a list of ValidationError. It is empty if no errors * have been found. * * @param handler * @return * @throws ValidationException */ public List<ValidationError> validateOutputIntent(DocumentHandler handler) throws ValidationException { List<ValidationError> result = new ArrayList<ValidationError>(0); PDDocument pdDocument = handler.getDocument(); PDDocumentCatalog catalog = pdDocument.getDocumentCatalog(); COSDocument cDoc = pdDocument.getDocument(); COSBase cBase = catalog.getCOSDictionary().getItem( COSName.getPDFName(DOCUMENT_DICTIONARY_KEY_OUTPUT_INTENTS)); COSArray outputIntents = COSUtils.getAsArray(cBase, cDoc); Map<COSObjectKey, Boolean> tmpDestOutputProfile = new HashMap<COSObjectKey, Boolean>(); for (int i = 0; outputIntents != null && i < outputIntents.size(); ++i) { COSDictionary dictionary = COSUtils.getAsDictionary(outputIntents.get(i), cDoc); if (dictionary == null) { result.add(new ValidationError( ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "OutputIntent object is null or isn't a dictionary")); } else { // ---- S entry is mandatory and must be equals to GTS_PDFA1 String sValue = dictionary.getNameAsString(COSName .getPDFName(OUTPUT_INTENT_DICTIONARY_KEY_S)); if (!OUTPUT_INTENT_DICTIONARY_VALUE_GTS_PDFA1.equals(sValue)) { result.add(new ValidationError( ERROR_GRAPHIC_OUTPUT_INTENT_S_VALUE_INVALID,"The S entry of the OutputIntent isn't GTS_PDFA1")); continue; } // ---- OutputConditionIdentifier is a mandatory field String outputConditionIdentifier = dictionary .getString(COSName .getPDFName(OUTPUT_INTENT_DICTIONARY_KEY_OUTPUT_CONDITION_IDENTIFIER)); if (outputConditionIdentifier == null || "".equals(outputConditionIdentifier)) { result.add(new ValidationError( ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "The OutputIntentCondition is missing")); continue; } // ---- If OutputConditionIdentifier is "Custom" : // ---- DestOutputProfile and Info are mandatory // ---- DestOutputProfile must be a ICC Profile // ---- Because of PDF/A conforming file needs to specify the color // characteristics, the DestOutputProfile // is checked even if the OutputConditionIdentifier isn't "Custom" COSBase dop = dictionary.getItem(COSName .getPDFName(OUTPUT_INTENT_DICTIONARY_KEY_DEST_OUTPUT_PROFILE)); ValidationError valer = validateICCProfile(dop, cDoc, tmpDestOutputProfile, handler); if (valer != null) { result.add(valer); continue; } if (OUTPUT_INTENT_DICTIONARY_VALUE_OUTPUT_CONDITION_IDENTIFIER_CUSTOM .equals(outputConditionIdentifier)) { String info = dictionary.getString(COSName .getPDFName(OUTPUT_INTENT_DICTIONARY_KEY_INFO)); if (info == null || "".equals(info)) { result.add(new ValidationError( ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "The Info entry of a OutputIntent dictionary is missing")); continue; } } } } return result; } /** * This method checks the destOutputProfile which must be a valid ICCProfile. * * If an other ICCProfile exists in the mapDestOutputProfile, a * ValdiationError (ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_MULTIPLE) is * returned because of only one profile is authorized. If the ICCProfile * already exist in the mapDestOutputProfile, the method returns null. If the * destOutputProfile contains an invalid ICCProfile, a ValidationError * (ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_INVALID) is returned If the * destOutputProfile is an empty stream, a * ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY) is returned. * * If the destOutputFile is valid, mapDestOutputProfile is updated, the * ICCProfile is added to the document handler and null is returned. * * @param destOutputProfile * @param cDoc * @param tmpDestOutputProfile * @param handler * @return * @throws ValidationException */ protected ValidationError validateICCProfile(COSBase destOutputProfile, COSDocument cDoc, Map<COSObjectKey, Boolean> mapDestOutputProfile, DocumentHandler handler) throws ValidationException { try { if (destOutputProfile == null) { return new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "OutputIntent object uses a NULL Object"); } // ---- destOutputProfile should be an instance of COSObject because of // this is a object reference if (destOutputProfile instanceof COSObject) { if (mapDestOutputProfile.containsKey(new COSObjectKey( (COSObject) destOutputProfile))) { // ---- the profile is already checked. continue return null; } else if (!mapDestOutputProfile.isEmpty()) { // ---- A DestOutputProfile exits but it isn't the same, error return new ValidationError( ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_MULTIPLE, "More than one ICCProfile is defined"); } // else the profile will be kept in the tmpDestOutputProfile if it is valid } PDStream stream = PDStream.createFromCOS(COSUtils.getAsStream( destOutputProfile, cDoc)); if (stream == null) { return new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "OutputIntent object uses a NULL Object"); } ICC_Profile iccp = ICC_Profile.getInstance(stream.getByteArray()); // check the ICC Profile version (6.2.2) if (iccp.getMajorVersion() == 2) { if (iccp.getMinorVersion() > 0x20) { // in PDF 1.4, max version is 02h.20h (meaning V 3.5) return new ValidationError( ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_TOO_RECENT, "Invalid version of the ICCProfile"); } // else OK } else if (iccp.getMajorVersion() > 2) { // in PDF 1.4, max version is 02h.20h (meaning V 3.5) return new ValidationError( ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_TOO_RECENT, "Invalid version of the ICCProfile"); } // else seems less than 2, so correct if (handler.getIccProfileWrapper() == null) { handler.setIccProfileWrapper(new ICCProfileWrapper(iccp)); } // ---- keep reference to avoid multiple profile definition mapDestOutputProfile.put(new COSObjectKey((COSObject) destOutputProfile), true); } catch (IllegalArgumentException e) { // ---- this is not a ICC_Profile return new ValidationError( ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_INVALID, "DestOutputProfile isn't a ICCProfile"); } catch (IOException e) { throw new ValidationException("Unable to parse the ICC Profile", e); } return null; } }