/*****************************************************************************
*
* 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.reflect;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.apache.pdfbox.preflight.PreflightConfiguration.FONT_PROCESS;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_GRAPHIC_UNEXPECTED_KEY;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_BLEND_MODE;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_CA;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_SOFT_MASK;
import static org.apache.pdfbox.preflight.PreflightConstants.TRANPARENCY_DICTIONARY_KEY_EXTGSTATE_ENTRY_REGEX;
import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_BLEND_MODE;
import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_LOWER_CA;
import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_UPPER_CA;
import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_VALUE_BM_COMPATIBLE;
import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_VALUE_BM_NORMAL;
import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_VALUE_SOFT_MASK_NONE;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_COMMON;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_NUMERIC_RANGE;
import static org.apache.pdfbox.preflight.PreflightConstants.MAX_NEGATIVE_FLOAT;
import static org.apache.pdfbox.preflight.PreflightConstants.MAX_POSITIVE_FLOAT;
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.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontFactory;
import org.apache.pdfbox.preflight.PreflightConstants;
import org.apache.pdfbox.preflight.PreflightContext;
import org.apache.pdfbox.preflight.PreflightPath;
import org.apache.pdfbox.preflight.ValidationResult.ValidationError;
import org.apache.pdfbox.preflight.exception.ValidationException;
import org.apache.pdfbox.preflight.process.AbstractProcess;
import org.apache.pdfbox.preflight.utils.COSUtils;
import org.apache.pdfbox.preflight.utils.ContextHelper;
public class ExtGStateValidationProcess extends AbstractProcess
{
/**
* Validate the ExtGState dictionaries.
*
* @param context the context which contains the Resource dictionary.
* @throws ValidationException thrown if a the Extended Graphic State isn't valid.
*/
@Override
public void validate(PreflightContext context) throws ValidationException
{
PreflightPath vPath = context.getValidationPath();
if (vPath.isEmpty())
{
return;
}
if (!vPath.isExpectedType(COSDictionary.class))
{
context.addValidationError(new ValidationError(PreflightConstants.ERROR_GRAPHIC_XOBJECT_INVALID_TYPE, "ExtGState validation required at least a Resource dictionary"));
}
else
{
COSDictionary extGStatesDict = (COSDictionary) vPath.peek();
List<COSDictionary> listOfExtGState = extractExtGStateDictionaries(context, extGStatesDict);
validateTransparencyRules(context, listOfExtGState);
validateFonts(context, listOfExtGState);
}
}
/**
* Create a list of ExtGState dictionaries using the given Resource dictionary and the COSDocument.
*
* @param context the context which contains the Resource dictionary.
* @param egsEntry a resource COSDictionary.
* @return the list of ExtGState dictionaries.
* @throws ValidationException thrown if a the Extended Graphic State isn't valid.
*/
public List<COSDictionary> extractExtGStateDictionaries(PreflightContext context, COSDictionary egsEntry)
throws ValidationException
{
List<COSDictionary> listOfExtGState = new ArrayList<>(0);
COSDocument cosDocument = context.getDocument().getDocument();
COSDictionary extGStates = COSUtils.getAsDictionary(egsEntry, cosDocument);
if (extGStates != null)
{
for (Object object : extGStates.keySet())
{
COSName key = (COSName) object;
if (key.getName().matches(TRANPARENCY_DICTIONARY_KEY_EXTGSTATE_ENTRY_REGEX))
{
COSBase gsBase = extGStates.getItem(key);
COSDictionary gsDict = COSUtils.getAsDictionary(gsBase, cosDocument);
if (gsDict == null)
{
throw new ValidationException("The Extended Graphics State dictionary is invalid");
}
listOfExtGState.add(gsDict);
}
}
}
return listOfExtGState;
}
/**
* Validate transparency rules in all ExtGState dictionaries of this container.
*
* @param context the preflight context.
* @param listOfExtGState a list of ExtGState COSDictionaries.
*
*/
protected void validateTransparencyRules(PreflightContext context, List<COSDictionary> listOfExtGState)
{
for (COSDictionary egs : listOfExtGState)
{
checkSoftMask(context, egs);
checkUpperCA(context, egs);
checkLowerCA(context, egs);
checkBlendMode(context, egs);
checkTRKey(context, egs);
checkTR2Key(context, egs);
}
}
/**
* Validate fonts in all ExtGState dictionaries of this container.
*
* @param context the preflight context.
* @param listOfExtGState a list of ExtGState COSDictionaries.
* @throws ValidationException
*
*/
protected void validateFonts(PreflightContext context, List<COSDictionary> listOfExtGState)
throws ValidationException
{
for (COSDictionary egs : listOfExtGState)
{
checkFont(context, egs);
}
}
/**
* This method checks a Font array in the ExtGState dictionary.
*
* @param context the preflight context.
* @param egs the Graphic state to check
* @throws ValidationException
*/
private void checkFont(PreflightContext context, COSDictionary egs) throws ValidationException
{
COSBase base = egs.getItem(COSName.FONT);
if (base == null)
{
return;
}
if (!(base instanceof COSArray) || ((COSArray) base).size() != 2)
{
context.addValidationError(new ValidationError(ERROR_SYNTAX_COMMON,
"/Font entry in /ExtGState must be an array with 2 elements"));
return;
}
COSArray ar = (COSArray) base;
COSBase base0 = ar.get(0);
if (!(base0 instanceof COSObject))
{
context.addValidationError(new ValidationError(ERROR_SYNTAX_COMMON,
"1st element in /Font entry in /ExtGState must be an indirect object"));
return;
}
COSBase base1 = ar.getObject(1);
if (!(base1 instanceof COSNumber))
{
context.addValidationError(new ValidationError(ERROR_SYNTAX_COMMON,
"2nd element in /Font entry in /ExtGState must be a number"));
return;
}
COSNumber fontSize = (COSNumber) ar.getObject(1);
if (fontSize.floatValue() > MAX_POSITIVE_FLOAT || fontSize.floatValue() < MAX_NEGATIVE_FLOAT)
{
context.addValidationError(new ValidationError(ERROR_SYNTAX_NUMERIC_RANGE,
"invalid float range in 2nd element in /Font entry in /ExtGState"));
}
if (ar.getObject(0) instanceof COSDictionary)
{
COSDictionary fontDict = (COSDictionary) ar.getObject(0);
try
{
PDFont newFont = PDFontFactory.createFont(fontDict);
ContextHelper.validateElement(context, newFont, FONT_PROCESS);
}
catch (IOException e)
{
addFontError(fontDict, context, e);
}
}
}
/**
* This method checks the SMask value of the ExtGState dictionary. The Soft Mask is optional but must be "None" if
* it is present.
*
* @param context the preflight context.
* @param egs the Graphic state to check
*/
private void checkSoftMask(PreflightContext context, COSDictionary egs)
{
COSBase smVal = egs.getItem(COSName.SMASK);
if (smVal != null &&
!(smVal instanceof COSName && TRANSPARENCY_DICTIONARY_VALUE_SOFT_MASK_NONE.equals(((COSName) smVal).getName())))
{
// ---- Soft Mask is valid only if it is a COSName equals to None
context.addValidationError(new ValidationError(ERROR_TRANSPARENCY_EXT_GS_SOFT_MASK,
"SoftMask must be null or None"));
}
}
/**
* This method checks the BM value of the ExtGState dictionary. The Blend Mode is optional but must be "Normal" or
* "Compatible" if it is present.
*
* @param context the preflight context * @param egs the graphic state to check
*/
private void checkBlendMode(PreflightContext context, COSDictionary egs)
{
COSBase bmVal = egs.getItem(TRANSPARENCY_DICTIONARY_KEY_BLEND_MODE);
if (bmVal != null)
{
// ---- Blend Mode is valid only if it is equals to Normal or Compatible
if (!(bmVal instanceof COSName && (TRANSPARENCY_DICTIONARY_VALUE_BM_NORMAL.equals(((COSName) bmVal)
.getName()) || TRANSPARENCY_DICTIONARY_VALUE_BM_COMPATIBLE.equals(((COSName) bmVal).getName()))))
{
context.addValidationError(new ValidationError(ERROR_TRANSPARENCY_EXT_GS_BLEND_MODE,
"BlendMode value isn't valid (only Normal and Compatible are authorized)"));
}
}
}
/**
* This method checks the "CA" value of the ExtGState dictionary. It is optional but must be 1.0
* if present.
*
* @param context the preflight context.
* @param egs the graphic state to check
*/
private void checkUpperCA(PreflightContext context, COSDictionary egs)
{
COSBase uCA = egs.getItem(TRANSPARENCY_DICTIONARY_KEY_UPPER_CA);
if (uCA != null)
{
// ---- If CA is present only the value 1.0 is authorized
COSDocument cosDocument = context.getDocument().getDocument();
Float fca = COSUtils.getAsFloat(uCA, cosDocument);
Integer ica = COSUtils.getAsInteger(uCA, cosDocument);
if (!(fca != null && fca == 1.0f) && !(ica != null && ica == 1))
{
context.addValidationError(new ValidationError(ERROR_TRANSPARENCY_EXT_GS_CA,
"CA entry in a ExtGState is invalid"));
}
}
}
/**
* This method checks the "ca" value of the ExtGState dictionary. It is optional but must be 1.0
* if present.
*
* @param context the preflight context.
* @param egs the graphic state to check
*/
private void checkLowerCA(PreflightContext context, COSDictionary egs)
{
COSBase lCA = egs.getItem(TRANSPARENCY_DICTIONARY_KEY_LOWER_CA);
if (lCA != null)
{
// ---- If ca is present only the value 1.0 is authorized
COSDocument cosDocument = context.getDocument().getDocument();
Float fca = COSUtils.getAsFloat(lCA, cosDocument);
Integer ica = COSUtils.getAsInteger(lCA, cosDocument);
if (!(fca != null && fca == 1.0f) && !(ica != null && ica == 1))
{
context.addValidationError(new ValidationError(ERROR_TRANSPARENCY_EXT_GS_CA,
"ca entry in a ExtGState is invalid"));
}
}
}
/**
* Check the TR entry. A valid ExtGState hasn't TR entry.
*
* @param context the preflight context
* @param egs the graphic state to check
*/
protected void checkTRKey(PreflightContext context, COSDictionary egs)
{
if (egs.getItem(COSName.TR) != null)
{
context.addValidationError(new ValidationError(ERROR_GRAPHIC_UNEXPECTED_KEY,
"No TR key expected in Extended graphics state"));
}
}
/**
* Check the TR2 entry. A valid ExtGState hasn't TR2 entry or a TR2 entry equals to "default".
*
* @param context the preflight context
* @param egs the graphic state to check
*/
protected void checkTR2Key(PreflightContext context, COSDictionary egs)
{
if (egs.getItem("TR2") != null)
{
String s = egs.getNameAsString("TR2");
if (!"Default".equals(s))
{
context.addValidationError(new ValidationError(ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY,
"TR2 key only expect 'Default' value, not '" + s + "'"));
}
}
}
}