/*******************************************************************************
* 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.utils;
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.ValidationResult.ValidationError;
import net.padaf.preflight.contentstream.ContentStreamException;
import net.padaf.preflight.contentstream.StubOperator;
import net.padaf.preflight.graphics.ICCProfileWrapper;
import net.padaf.preflight.graphics.color.ColorSpaceHelper;
import net.padaf.preflight.graphics.color.ColorSpaceHelperFactory;
import net.padaf.preflight.graphics.color.ColorSpaces;
import net.padaf.preflight.graphics.color.ColorSpaceHelperFactory.ColorSpaceRestriction;
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.COSString;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.util.PDFOperator;
import org.apache.pdfbox.util.PDFStreamEngine;
import org.apache.pdfbox.util.operator.BeginText;
import org.apache.pdfbox.util.operator.Concatenate;
import org.apache.pdfbox.util.operator.EndText;
import org.apache.pdfbox.util.operator.GRestore;
import org.apache.pdfbox.util.operator.GSave;
import org.apache.pdfbox.util.operator.Invoke;
import org.apache.pdfbox.util.operator.MoveText;
import org.apache.pdfbox.util.operator.MoveTextSetLeading;
import org.apache.pdfbox.util.operator.NextLine;
import org.apache.pdfbox.util.operator.OperatorProcessor;
import org.apache.pdfbox.util.operator.SetCharSpacing;
import org.apache.pdfbox.util.operator.SetHorizontalTextScaling;
import org.apache.pdfbox.util.operator.SetLineCapStyle;
import org.apache.pdfbox.util.operator.SetLineDashPattern;
import org.apache.pdfbox.util.operator.SetLineJoinStyle;
import org.apache.pdfbox.util.operator.SetLineWidth;
import org.apache.pdfbox.util.operator.SetMatrix;
import org.apache.pdfbox.util.operator.SetNonStrokingCMYKColor;
import org.apache.pdfbox.util.operator.SetNonStrokingColor;
import org.apache.pdfbox.util.operator.SetNonStrokingColorSpace;
import org.apache.pdfbox.util.operator.SetNonStrokingRGBColor;
import org.apache.pdfbox.util.operator.SetStrokingCMYKColor;
import org.apache.pdfbox.util.operator.SetStrokingColor;
import org.apache.pdfbox.util.operator.SetStrokingColorSpace;
import org.apache.pdfbox.util.operator.SetStrokingRGBColor;
import org.apache.pdfbox.util.operator.SetTextFont;
import org.apache.pdfbox.util.operator.SetTextLeading;
import org.apache.pdfbox.util.operator.SetTextRenderingMode;
import org.apache.pdfbox.util.operator.SetTextRise;
import org.apache.pdfbox.util.operator.SetWordSpacing;
import static net.padaf.preflight.ValidationConstants.*;
/**
* This class inherits from org.apache.pdfbox.util.PDFStreamEngine to allow the
* validation of specific rules in ContentStream.
*/
public abstract class ContentStreamEngine extends PDFStreamEngine {
protected DocumentHandler documentHandler = null;
protected Map<String,OperatorProcessor> contentStreamEngineOperators = new HashMap<String,OperatorProcessor>();
public ContentStreamEngine(DocumentHandler _handler) {
this.documentHandler = _handler;
// ---- Graphics operators
registerOperatorProcessor("w", new SetLineWidth());
registerOperatorProcessor("cm", new Concatenate());
registerOperatorProcessor("CS", new SetStrokingColorSpace());
registerOperatorProcessor("cs", new SetNonStrokingColorSpace());
registerOperatorProcessor("d", new SetLineDashPattern());
registerOperatorProcessor("Do", new Invoke());
registerOperatorProcessor("j", new SetLineJoinStyle());
registerOperatorProcessor("J", new SetLineCapStyle());
registerOperatorProcessor("K", new SetStrokingCMYKColor());
registerOperatorProcessor("k", new SetNonStrokingCMYKColor());
registerOperatorProcessor("rg", new SetNonStrokingRGBColor());
registerOperatorProcessor("RG", new SetStrokingRGBColor());
registerOperatorProcessor("SC", new SetStrokingColor());
registerOperatorProcessor("SCN", new SetStrokingColor());
registerOperatorProcessor("sc", new SetNonStrokingColor());
registerOperatorProcessor("scn", new SetNonStrokingColor());
// ---- Graphics state
registerOperatorProcessor("Q", new GRestore());
registerOperatorProcessor("q", new GSave());
// ---- Text operators
registerOperatorProcessor("BT", new BeginText());
registerOperatorProcessor("ET", new EndText());
registerOperatorProcessor("Tf", new SetTextFont());
registerOperatorProcessor("Tr", new SetTextRenderingMode());
registerOperatorProcessor("Tm", new SetMatrix());
registerOperatorProcessor("Td", new MoveText());
registerOperatorProcessor("T*", new NextLine());
registerOperatorProcessor("TD", new MoveTextSetLeading());
registerOperatorProcessor("Tc", new SetCharSpacing());
registerOperatorProcessor("TL", new SetTextLeading());
registerOperatorProcessor("Ts", new SetTextRise());
registerOperatorProcessor("Tw", new SetWordSpacing());
registerOperatorProcessor("Tz", new SetHorizontalTextScaling());
// ---- Do not use the PDFBox Operator, because of the PageDrawer class cast
// Or because the Operator doesn't exist
StubOperator so = new StubOperator();
registerOperatorProcessor("l", so);
registerOperatorProcessor("re", so);
registerOperatorProcessor("c", so);
registerOperatorProcessor("y", so);
registerOperatorProcessor("v", so);
registerOperatorProcessor("n", so);
registerOperatorProcessor("BI", so);
registerOperatorProcessor("EI", so);
registerOperatorProcessor("m", so);
registerOperatorProcessor("W*", so);
registerOperatorProcessor("W", so);
registerOperatorProcessor("h", so);
registerOperatorProcessor("Tj", so);
registerOperatorProcessor("TJ", so);
registerOperatorProcessor("'", so);
registerOperatorProcessor("\"", so);
registerOperatorProcessor("b", so);
registerOperatorProcessor("B", so);
registerOperatorProcessor("b*", so);
registerOperatorProcessor("B*", so);
registerOperatorProcessor("BDC", so);
registerOperatorProcessor("BMC", so);
registerOperatorProcessor("DP", so);
registerOperatorProcessor("EMC", so);
registerOperatorProcessor("d0", so);
registerOperatorProcessor("d1", so);
registerOperatorProcessor("f", so);
registerOperatorProcessor("F", so);
registerOperatorProcessor("f*", so);
registerOperatorProcessor("g", so);
registerOperatorProcessor("G", so);
registerOperatorProcessor("M", so);
registerOperatorProcessor("MP", so);
registerOperatorProcessor("gs", so);
registerOperatorProcessor("h", so);
registerOperatorProcessor("i", so);
registerOperatorProcessor("ri", so);
registerOperatorProcessor("s", so);
registerOperatorProcessor("S", so);
registerOperatorProcessor("sh", so);
}
public final void registerOperatorProcessor( String operator, OperatorProcessor op )
{
super.registerOperatorProcessor(operator, op);
contentStreamEngineOperators.put( operator, op );
}
/**
* Check operands of the "ri" operator. Operands must exist in the
* RenderingIntent list. (net.awl.edoc.pdfa.validation.utils.RenderingIntents)
*
* @param operator
* the "ri" operator
* @param arguments
* the "ri" operands
* @throws ContentStreamException
* ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY if the operand is invalid
*/
protected void validRenderingIntent(PDFOperator operator, List arguments)
throws ContentStreamException {
if ("ri".equals(operator.getOperation())) {
if (!RenderingIntents.contains(arguments.get(0))) {
throwContentStreamException("Unexpected value '" + arguments.get(0)
+ "' for ri operand. ", ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY);
}
}
}
/**
* Valid the number of graphic states if the operator is the Save Graphic state operator ("q")
* @param operator
* @throws ContentStreamException
*/
protected void validNumberOfGraphicStates(PDFOperator operator) throws ContentStreamException {
if ("q".equals(operator.getOperation())) {
int numberOfGraphicStates = this.getGraphicsStack().size();
if (numberOfGraphicStates > MAX_GRAPHIC_STATES) {
throwContentStreamException("Too many graphic states", ERROR_GRAPHIC_TOO_MANY_GRAPHIC_STATES);
}
}
}
/**
* Throw a ContentStreamException if the LZW filter is used in a InlinedImage.
*
* @param operator
* the InlinedImage object (BI to EI)
* @throws ContentStreamException
*/
protected void validImageFilter(PDFOperator operator)
throws ContentStreamException {
COSDictionary dict = operator.getImageParameters().getDictionary();
// ---- Search a Filter declaration in the InlinedImage dictionary.
// ---- The LZWDecode Filter is forbidden.
String filter = dict.getNameAsString(STREAM_DICTIONARY_KEY_F);
if (filter == null) {
filter = dict.getNameAsString(STREAM_DICTIONARY_KEY_FILTER);
}
String errorCode = FilterHelper.isAuthorizedFilter(filter);
if (errorCode != null) {
// --- LZW is forbidden.
if ( ERROR_SYNTAX_STREAM_INVALID_FILTER.equals(errorCode) ) {
throwContentStreamException("LZW filter can't be used in a PDF/A File", ERROR_SYNTAX_STREAM_INVALID_FILTER);
} else {
throwContentStreamException("This filter isn't defined in the PDF Reference Third Edition.", ERROR_SYNTAX_STREAM_UNDEFINED_FILTER);
}
}
}
/**
* This method validates if the ColorSpace used by the InlinedImage is
* consistent with the color space defined in OutputIntent dictionaries.
*
* @param operator
* the InlinedImage object (BI to EI)
* @throws ContentStreamException
*/
protected void validImageColorSpace(PDFOperator operator)
throws ContentStreamException, IOException {
COSDictionary dict = operator.getImageParameters().getDictionary();
COSDocument doc = this.documentHandler.getDocument().getDocument();
COSBase csInlinedBase = dict.getItem(COSName
.getPDFName(STREAM_DICTIONARY_KEY_COLOR_SPACE));
ColorSpaceHelper csHelper = null;
if (csInlinedBase != null) {
if (COSUtils.isString(csInlinedBase, doc)) {
// ---- In InlinedImage only DeviceGray/RGB/CMYK and restricted Indexed
// color spaces
// are allowed.
String colorSpace = COSUtils.getAsString(csInlinedBase, doc);
ColorSpaces cs = null;
try {
cs = ColorSpaces.valueOf(colorSpace);
} catch (IllegalArgumentException e) {
// ---- The color space is unknown.
// ---- Try to access the resources dictionary, the color space can be
// a reference.
PDColorSpace pdCS = (PDColorSpace) this.getColorSpaces().get(
colorSpace);
if (pdCS != null) {
cs = ColorSpaces.valueOf(pdCS.getName());
csHelper = ColorSpaceHelperFactory.getColorSpaceHelper(pdCS,
documentHandler, ColorSpaceRestriction.ONLY_DEVICE);
}
}
if (cs == null) {
throwContentStreamException("The ColorSpace is unknown",
ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY);
}
}
if (csHelper == null) {
csHelper = ColorSpaceHelperFactory.getColorSpaceHelper(csInlinedBase,
documentHandler, ColorSpaceRestriction.ONLY_DEVICE);
}
List<ValidationError> errors = new ArrayList<ValidationError>();
try {
if (!csHelper.validate(errors)) {
ValidationError ve = errors.get(0);
throwContentStreamException(ve.getDetails(), ve.getErrorCode());
}
} catch (ValidationException e) {
throw new IOException(e.getMessage());
}
}
}
/**
* This method validates if the ColorOperator can be used with the color space
* defined in OutputIntent dictionaries.
*
* @param operator
* the color operator
* @throws ContentStreamException
*/
protected void checkColorOperators(String operation)
throws ContentStreamException {
if ("rg".equals(operation) || "RG".equals(operation)) {
ICCProfileWrapper iccpw = documentHandler.getIccProfileWrapper();
if (iccpw == null || !iccpw.isRGBColorSpace()) {
throwContentStreamException("The operator \"" + operation
+ "\" can't be used with CMYK Profile",
ERROR_GRAPHIC_INVALID_COLOR_SPACE_RGB);
}
}
if ("k".equals(operation) || "K".equals(operation)) {
ICCProfileWrapper iccpw = documentHandler.getIccProfileWrapper();
if (iccpw == null || !iccpw.isCMYKColorSpace()) {
throwContentStreamException("The operator \"" + operation
+ "\" can't be used with RGB Profile",
ERROR_GRAPHIC_INVALID_COLOR_SPACE_CMYK);
}
}
if ("g".equals(operation) || "G".equals(operation)) {
ICCProfileWrapper iccpw = documentHandler.getIccProfileWrapper();
if (iccpw == null) {
// ---- Gray is possible with RGB and CMYK color space
throwContentStreamException("The operator \"" + operation
+ "\" can't be used without Color Profile",
ERROR_GRAPHIC_INVALID_COLOR_SPACE_MISSING);
}
}
if ("f".equals(operation) || "F".equals(operation)
|| "f*".equals(operation) || "B".equals(operation)
|| "B*".equals(operation) || "b".equals(operation)
|| "b*".equals(operation)) {
ICCProfileWrapper iccpw = documentHandler.getIccProfileWrapper();
if (iccpw == null) {
// ---- The default fill color needs an OutputIntent
throwContentStreamException("The operator \"" + operation
+ "\" can't be used without Color Profile",
ERROR_GRAPHIC_INVALID_COLOR_SPACE_MISSING);
}
}
}
/**
* This method validates if the ColorSpace used as operand is consistent with
* the color space defined in OutputIntent dictionaries.
*
* @param operator
* @param arguments
* @throws IOException
*/
protected void checkSetColorSpaceOperators(PDFOperator operator,
List<?> arguments) throws IOException {
if (!("CS".equals(operator.getOperation()) || "cs".equals(operator
.getOperation()))) {
return;
}
String colorSpaceName = null;
if (arguments.get(0) instanceof String) {
colorSpaceName = (String) arguments.get(0);
} else if (arguments.get(0) instanceof COSString) {
colorSpaceName = ((COSString) arguments.get(0)).toString();
} else if (arguments.get(0) instanceof COSName) {
colorSpaceName = ((COSName) arguments.get(0)).getName();
} else {
throwContentStreamException("The operand doesn't have the expected type",
ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY);
}
ColorSpaceHelper csHelper = null;
ColorSpaces cs = null;
try {
cs = ColorSpaces.valueOf(colorSpaceName);
} catch (IllegalArgumentException e) {
// ---- The color space is unknown.
// ---- Try to access the resources dictionary, the color space can be a
// reference.
PDColorSpace pdCS = (PDColorSpace) this.getColorSpaces().get(
colorSpaceName);
if (pdCS != null) {
cs = ColorSpaces.valueOf(pdCS.getName());
csHelper = ColorSpaceHelperFactory.getColorSpaceHelper(pdCS,
documentHandler, ColorSpaceRestriction.NO_RESTRICTION);
}
}
if (cs == null) {
throwContentStreamException("The ColorSpace is unknown",
ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY);
}
if (csHelper == null) {
csHelper = ColorSpaceHelperFactory.getColorSpaceHelper(COSName
.getPDFName(colorSpaceName), documentHandler,
ColorSpaceRestriction.NO_RESTRICTION);
}
List<ValidationError> errors = new ArrayList<ValidationError>();
try {
if (!csHelper.validate(errors)) {
ValidationError ve = errors.get(0);
throwContentStreamException(ve.getDetails(), ve.getErrorCode());
}
} catch (ValidationException e) {
// throw new IOException(e.getMessage(), e); java 6
throw new IOException(e.getMessage());
}
}
/**
* Build a ContentStreamException using the given parameters
*
* @param msg
* exception details
* @param errorCode
* the error code.
* @throws ContentStreamException
*/
protected void throwContentStreamException(String msg, String errorCode)
throws ContentStreamException {
ContentStreamException cex = new ContentStreamException(msg);
cex.setValidationError(errorCode);
throw cex;
}
}