/***************************************************************************** * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF 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 org.apache.pdfbox.preflight.process; import java.awt.color.ICC_Profile; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; 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.COSNumber; import org.apache.pdfbox.cos.COSObject; import org.apache.pdfbox.cos.COSObjectKey; import org.apache.pdfbox.cos.COSStream; 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.preflight.PreflightConfiguration; import org.apache.pdfbox.preflight.PreflightContext; import org.apache.pdfbox.preflight.ValidationResult.ValidationError; import org.apache.pdfbox.preflight.exception.ValidationException; import org.apache.pdfbox.preflight.graphic.ICCProfileWrapper; import org.apache.pdfbox.preflight.utils.COSUtils; import org.apache.pdfbox.preflight.utils.ContextHelper; import static org.apache.pdfbox.preflight.PreflightConfiguration.ACTIONS_PROCESS; import static org.apache.pdfbox.preflight.PreflightConstants.*; /** * This ValidationProcess check if the Catalog entries are confirming with the PDF/A-1b specification. */ public class CatalogValidationProcess extends AbstractProcess { protected PDDocumentCatalog catalog; protected List<String> listICC = new ArrayList<>(); public CatalogValidationProcess() { listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA43); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR_006); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR006); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA39); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_JC200103); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA27); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_EUROSB104); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA45); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA46); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA41); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR_001); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR_003); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR_005); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR001); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR003); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR005); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA28); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_JCW2003); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_EUROSB204); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA47); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA44); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA29); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_JC200104); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA40); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA30); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA42); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_IFRA26); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_JCN2002); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR_002); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_CGATS_TR002); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA33); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA37); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA31); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA35); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA32); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA34); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA36); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_FOGRA38); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_sRGB); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_sRGB_IEC); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_Adobe); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_bg_sRGB); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_sYCC); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_scRGB); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_scRGB_nl); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_scYCC_nl); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_ROMM); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_RIMM); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_ERIMM); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_eciRGB); listICC.add(ICC_CHARACTERIZATION_DATA_REGISTRY_opRGB); } protected boolean isStandardICCCharacterization(String name) { for (String iccStandard : listICC) { if (iccStandard.contains(name)) { return true; } } return false; } @Override public void validate(PreflightContext ctx) throws ValidationException { PDDocument pdfbox = ctx.getDocument(); this.catalog = pdfbox.getDocumentCatalog(); if (this.catalog == null) { ctx.addValidationError(new ValidationError(ERROR_SYNTAX_NOCATALOG, "There are no Catalog entry in the Document")); } else { validateActions(ctx); validateLang(ctx); validateNames(ctx); validateOCProperties(ctx); validateOutputIntent(ctx); } } /** * This method validates if OpenAction entry contains forbidden action type. It checks too if an Additional Action * is present. * * @param ctx * @throws ValidationException * Propagate the ActionManager exception */ protected void validateActions(PreflightContext ctx) throws ValidationException { ContextHelper.validateElement(ctx, catalog.getCOSObject(), ACTIONS_PROCESS); // AA entry if forbidden in PDF/A-1 COSBase aa = catalog.getCOSObject().getItem(DICTIONARY_KEY_ADDITIONAL_ACTION); if (aa != null) { addValidationError(ctx, new ValidationError(ERROR_ACTION_FORBIDDEN_ADDITIONAL_ACTION, "The AA field is forbidden for the Catalog when the PDF is a PDF/A")); } } /** * The Lang element is optional but it is recommended. This method check the Syntax of the Lang if this entry is * present. * * @param ctx * @throws ValidationException */ protected void validateLang(PreflightContext ctx) throws ValidationException { String lang = catalog.getLanguage(); if (lang != null && !"".equals(lang) && !lang.matches("[A-Za-z]{1,8}(-[A-Za-z]{1,8})*")) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_LANG_NOT_RFC1766)); } } /** * A Catalog shall not contain the EmbeddedFiles entry. * * @param ctx * @throws ValidationException */ protected void validateNames(PreflightContext ctx) throws ValidationException { PDDocumentNameDictionary names = catalog.getNames(); if (names != null) { PDEmbeddedFilesNameTreeNode efs = names.getEmbeddedFiles(); if (efs != null) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_CATALOG_EMBEDDEDFILES, "EmbeddedFile entry is present in the Names dictionary")); } if (names.getJavaScript() != null) { addValidationError(ctx, new ValidationError(ERROR_ACTION_FORBIDDEN_ACTIONS_NAMED, "Javascript entry is present in the Names dictionary")); } } } /** * A Catalog shall not contain the OCPProperties (Optional Content Properties) entry. * * @param ctx * @throws ValidationException */ protected void validateOCProperties(PreflightContext ctx) throws ValidationException { if (catalog.getOCProperties() != null) { addValidationError(ctx, 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 ctx * @throws ValidationException */ public void validateOutputIntent(PreflightContext ctx) throws ValidationException { COSDocument cosDocument = ctx.getDocument().getDocument(); COSBase cBase = catalog.getCOSObject().getItem(COSName.getPDFName(DOCUMENT_DICTIONARY_KEY_OUTPUT_INTENTS)); COSArray outputIntents = COSUtils.getAsArray(cBase, cosDocument); Map<COSObjectKey, Boolean> tmpDestOutputProfile = new HashMap<>(); for (int i = 0; outputIntents != null && i < outputIntents.size(); ++i) { COSDictionary outputIntentDict = COSUtils.getAsDictionary(outputIntents.get(i), cosDocument); if (outputIntentDict == null) { addValidationError(ctx, 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 = outputIntentDict.getNameAsString(OUTPUT_INTENT_DICTIONARY_KEY_S); if (!OUTPUT_INTENT_DICTIONARY_VALUE_GTS_PDFA1.equals(sValue)) { addValidationError(ctx, 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 = outputIntentDict .getString(OUTPUT_INTENT_DICTIONARY_KEY_OUTPUT_CONDITION_IDENTIFIER); if (outputConditionIdentifier == null) { // empty string is authorized (it may be an application specific value) addValidationError(ctx, new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "The OutputIntentCondition is missing")); continue; } /* * If OutputConditionIdentifier is "Custom" or a non Standard ICC Characterization : 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 destOutputProfile = outputIntentDict.getItem(OUTPUT_INTENT_DICTIONARY_KEY_DEST_OUTPUT_PROFILE); validateICCProfile(destOutputProfile, tmpDestOutputProfile, ctx); PreflightConfiguration config = ctx.getConfig(); if (config.isLazyValidation() && !isStandardICCCharacterization(outputConditionIdentifier)) { String info = outputIntentDict.getString(COSName.getPDFName(OUTPUT_INTENT_DICTIONARY_KEY_INFO)); if (info == null || "".equals(info)) { ValidationError error = new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "The Info entry of a OutputIntent dictionary is missing"); error.setWarning(true); addValidationError(ctx, error); continue; } } } } } /** * 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 ctx and * null is returned. * * @param destOutputProfile * @param mapDestOutputProfile * @param ctx the preflight context. * @throws ValidationException */ protected void validateICCProfile(COSBase destOutputProfile, Map<COSObjectKey, Boolean> mapDestOutputProfile, PreflightContext ctx) throws ValidationException { try { if (destOutputProfile == null) { return; } // 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; } else if (!mapDestOutputProfile.isEmpty()) { // A DestOutputProfile exits but it isn't the same, error addValidationError(ctx, new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_MULTIPLE, "More than one ICCProfile is defined")); return; } // else the profile will be kept in the tmpDestOutputProfile if it is valid } // keep reference to avoid multiple profile definition mapDestOutputProfile.put(new COSObjectKey((COSObject) destOutputProfile), true); COSDocument cosDocument = ctx.getDocument().getDocument(); COSStream stream = COSUtils.getAsStream(destOutputProfile, cosDocument); if (stream == null) { addValidationError(ctx, new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "OutputIntent object uses a NULL Object")); return; } InputStream is = stream.createInputStream(); ICC_Profile iccp = null; try { iccp = ICC_Profile.getInstance(is); } finally { is.close(); } if (!validateICCProfileNEntry(stream, ctx, iccp)) { return; } if (!validateICCProfileVersion(iccp, ctx)) { return; } if (ctx.getIccProfileWrapper() == null) { ctx.setIccProfileWrapper(new ICCProfileWrapper(iccp)); } } catch (IllegalArgumentException e) { // this is not a ICC_Profile addValidationError(ctx, new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_INVALID, "DestOutputProfile isn't a valid ICCProfile: " + e.getMessage(), e)); } catch (IOException e) { throw new ValidationException("Unable to parse the ICC Profile.", e); } } private boolean validateICCProfileVersion(ICC_Profile iccp, PreflightContext ctx) { PreflightConfiguration config = ctx.getConfig(); // check the ICC Profile version (6.2.2) if (iccp.getMajorVersion() == 2) { if (iccp.getMinorVersion() > 0x40) { // in PDF 1.4, max version is 02h.40h (meaning V 3.5) // see the ICCProfile specification (ICC.1:1998-09)page 13 - §6.1.3 : // The current profile version number is "2.4.0" (encoded as 02400000h") ValidationError error = new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_TOO_RECENT, "Invalid version of the ICCProfile"); error.setWarning(config.isLazyValidation()); addValidationError(ctx, error); return false; } // else OK } else if (iccp.getMajorVersion() > 2) { // in PDF 1.4, max version is 02h.40h (meaning V 3.5) // see the ICCProfile specification (ICC.1:1998-09)page 13 - §6.1.3 : // The current profile version number is "2.4.0" (encoded as 02400000h" ValidationError error = new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_ICC_PROFILE_TOO_RECENT, "Invalid version of the ICCProfile"); error.setWarning(config.isLazyValidation()); addValidationError(ctx, error); return false; } // else seems less than 2, so correct return true; } private boolean validateICCProfileNEntry(COSStream stream, PreflightContext ctx, ICC_Profile iccp) { COSDictionary streamDict = (COSDictionary) stream.getCOSObject(); if (!streamDict.containsKey(COSName.N)) { addValidationError(ctx, new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "/N entry of ICC profile is mandatory")); return false; } COSBase nValue = streamDict.getItem(COSName.N); if (!(nValue instanceof COSNumber)) { addValidationError(ctx, new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "/N entry of ICC profile must be a number, but is " + nValue)); return false; } int nNumberValue = ((COSNumber) nValue).intValue(); if (nNumberValue != 1 && nNumberValue != 3 && nNumberValue != 4) { addValidationError(ctx, new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "/N entry of ICC profile must be 1, 3 or 4, but is " + nNumberValue)); return false; } if (iccp.getNumComponents() != nNumberValue) { addValidationError(ctx, new ValidationError(ERROR_GRAPHIC_OUTPUT_INTENT_INVALID_ENTRY, "/N entry of ICC profile is " + nNumberValue + " but the ICC profile has " + iccp.getNumComponents() + " components")); return false; } return true; } }