/*
* 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.contentstream;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
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 java.util.Stack;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.contentstream.operator.MissingOperandException;
import org.apache.pdfbox.contentstream.operator.state.EmptyGraphicsStackException;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSNumber;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.filter.MissingImageReaderException;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdmodel.MissingResourceException;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontFactory;
import org.apache.pdfbox.pdmodel.font.PDType3CharProc;
import org.apache.pdfbox.pdmodel.font.PDType3Font;
import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup;
import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern;
import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState;
import org.apache.pdfbox.pdmodel.graphics.state.PDTextState;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.util.Matrix;
import org.apache.pdfbox.util.Vector;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.contentstream.operator.OperatorProcessor;
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
/**
* Processes a PDF content stream and executes certain operations.
* Provides a callback interface for clients that want to do things with the stream.
*
* @author Ben Litchfield
*/
public abstract class PDFStreamEngine
{
private static final Log LOG = LogFactory.getLog(PDFStreamEngine.class);
private final Map<String, OperatorProcessor> operators = new HashMap<>(80);
private Matrix textMatrix;
private Matrix textLineMatrix;
private Stack<PDGraphicsState> graphicsStack = new Stack<>();
private PDResources resources;
private PDPage currentPage;
private boolean isProcessingPage;
private Matrix initialMatrix;
/**
* Creates a new PDFStreamEngine.
*/
protected PDFStreamEngine()
{
}
/**
* Register a custom operator processor with the engine.
*
* @param operator The operator as a string.
* @param op Processor instance.
* @deprecated Use {@link #addOperator(OperatorProcessor)} instead
*/
@Deprecated
public void registerOperatorProcessor(String operator, OperatorProcessor op)
{
op.setContext(this);
operators.put(operator, op);
}
/**
* Adds an operator processor to the engine.
*
* @param op operator processor
*/
public final void addOperator(OperatorProcessor op)
{
op.setContext(this);
operators.put(op.getName(), op);
}
/**
* Initialises the stream engine for the given page.
*/
private void initPage(PDPage page)
{
if (page == null)
{
throw new IllegalArgumentException("Page cannot be null");
}
currentPage = page;
graphicsStack.clear();
graphicsStack.push(new PDGraphicsState(page.getCropBox()));
textMatrix = null;
textLineMatrix = null;
resources = null;
initialMatrix = page.getMatrix();
}
/**
* This will initialise and process the contents of the stream.
*
* @param page the page to process
* @throws IOException if there is an error accessing the stream
*/
public void processPage(PDPage page) throws IOException
{
initPage(page);
if (page.hasContents())
{
isProcessingPage = true;
processStream(page);
isProcessingPage = false;
}
}
/**
* Shows a transparency group from the content stream.
*
* @param form transparency group (form) XObject
* @throws IOException if the transparency group cannot be processed
*/
public void showTransparencyGroup(PDTransparencyGroup form) throws IOException
{
processTransparencyGroup(form);
}
/**
* Shows a form from the content stream.
*
* @param form form XObject
* @throws IOException if the form cannot be processed
*/
public void showForm(PDFormXObject form) throws IOException
{
if (currentPage == null)
{
throw new IllegalStateException("No current page, call " +
"#processChildStream(PDContentStream, PDPage) instead");
}
processStream(form);
}
/**
* Processes a soft mask transparency group stream.
* @param group
* @throws IOException
*/
protected void processSoftMask(PDTransparencyGroup group) throws IOException
{
saveGraphicsState();
Matrix softMaskCTM = getGraphicsState().getSoftMask().getInitialTransformationMatrix();
getGraphicsState().setCurrentTransformationMatrix(softMaskCTM);
processTransparencyGroup(group);
restoreGraphicsState();
}
/**
* Processes a transparency group stream.
* @param group
* @throws IOException
*/
protected void processTransparencyGroup(PDTransparencyGroup group) throws IOException
{
if (currentPage == null)
{
throw new IllegalStateException("No current page, call " +
"#processChildStream(PDContentStream, PDPage) instead");
}
PDResources parent = pushResources(group);
Stack<PDGraphicsState> savedStack = saveGraphicsStack();
Matrix parentMatrix = initialMatrix;
// the stream's initial matrix includes the parent CTM, e.g. this allows a scaled form
initialMatrix = getGraphicsState().getCurrentTransformationMatrix().clone();
// transform the CTM using the stream's matrix
getGraphicsState().getCurrentTransformationMatrix().concatenate(group.getMatrix());
// Before execution of the transparency group XObject’s content stream,
// the current blend mode in the graphics state shall be initialized to Normal,
// the current stroking and nonstroking alpha constants to 1.0, and the current soft mask to None.
getGraphicsState().setBlendMode(BlendMode.NORMAL);
getGraphicsState().setAlphaConstant(1);
getGraphicsState().setNonStrokeAlphaConstant(1);
getGraphicsState().setSoftMask(null);
// clip to bounding box
clipToRect(group.getBBox());
processStreamOperators(group);
initialMatrix = parentMatrix;
restoreGraphicsStack(savedStack);
popResources(parent);
}
/**
* Processes a Type 3 character stream.
*
* @param charProc Type 3 character procedure
* @param textRenderingMatrix the Text Rendering Matrix
* @throws IOException if there is an error reading or parsing the character content stream.
*/
protected void processType3Stream(PDType3CharProc charProc, Matrix textRenderingMatrix)
throws IOException
{
if (currentPage == null)
{
throw new IllegalStateException("No current page, call " +
"#processChildStream(PDContentStream, PDPage) instead");
}
PDResources parent = pushResources(charProc);
Stack<PDGraphicsState> savedStack = saveGraphicsStack();
// replace the CTM with the TRM
getGraphicsState().setCurrentTransformationMatrix(textRenderingMatrix);
// transform the CTM using the stream's matrix (this is the FontMatrix)
getGraphicsState().getCurrentTransformationMatrix().concatenate(charProc.getMatrix());
// note: we don't clip to the BBox as it is often wrong, see PDFBOX-1917
// save text matrices (Type 3 stream may contain BT/ET, see PDFBOX-2137)
Matrix textMatrixOld = textMatrix;
textMatrix = new Matrix();
Matrix textLineMatrixOld = textLineMatrix;
textLineMatrix = new Matrix();
processStreamOperators(charProc);
// restore text matrices
textMatrix = textMatrixOld;
textLineMatrix = textLineMatrixOld;
restoreGraphicsStack(savedStack);
popResources(parent);
}
/**
* Process the given annotation with the specified appearance stream.
*
* @param annotation The annotation containing the appearance stream to process.
* @param appearance The appearance stream to process.
* @throws IOException If there is an error reading or parsing the appearance content stream.
*/
protected void processAnnotation(PDAnnotation annotation, PDAppearanceStream appearance)
throws IOException
{
PDResources parent = pushResources(appearance);
Stack<PDGraphicsState> savedStack = saveGraphicsStack();
PDRectangle bbox = appearance.getBBox();
PDRectangle rect = annotation.getRectangle();
Matrix matrix = appearance.getMatrix();
// zero-sized rectangles are not valid
if (rect != null && rect.getWidth() > 0 && rect.getHeight() > 0 && bbox != null)
{
// transformed appearance box fixme: may be an arbitrary shape
Rectangle2D transformedBox = bbox.transform(matrix).getBounds2D();
// compute a matrix which scales and translates the transformed appearance box to align
// with the edges of the annotation's rectangle
Matrix a = Matrix.getTranslateInstance(rect.getLowerLeftX(), rect.getLowerLeftY());
a.concatenate(Matrix.getScaleInstance((float)(rect.getWidth() / transformedBox.getWidth()),
(float)(rect.getHeight() / transformedBox.getHeight())));
a.concatenate(Matrix.getTranslateInstance((float) -transformedBox.getX(),
(float) -transformedBox.getY()));
// Matrix shall be concatenated with A to form a matrix AA that maps from the appearance's
// coordinate system to the annotation's rectangle in default user space
//
// HOWEVER only the opposite order works for rotated pages with
// filled fields / annotations that have a matrix in the appearance stream, see PDFBOX-3083
Matrix aa = Matrix.concatenate(a, matrix);
// make matrix AA the CTM
getGraphicsState().setCurrentTransformationMatrix(aa);
// clip to bounding box
clipToRect(bbox);
// needed for patterns in appearance streams, e.g. PDFBOX-2182
initialMatrix = aa.clone();
processStreamOperators(appearance);
}
restoreGraphicsStack(savedStack);
popResources(parent);
}
/**
* Process the given tiling pattern.
*
* @param tilingPattern the tiling pattern
* @param color color to use, if this is an uncoloured pattern, otherwise null.
* @param colorSpace color space to use, if this is an uncoloured pattern, otherwise null.
* @throws IOException if there is an error reading or parsing the tiling pattern content stream.
*/
protected final void processTilingPattern(PDTilingPattern tilingPattern, PDColor color,
PDColorSpace colorSpace) throws IOException
{
processTilingPattern(tilingPattern, color, colorSpace, tilingPattern.getMatrix());
}
/**
* Process the given tiling pattern. Allows the pattern matrix to be overridden for custom
* rendering.
*
* @param tilingPattern the tiling pattern
* @param color color to use, if this is an uncoloured pattern, otherwise null.
* @param colorSpace color space to use, if this is an uncoloured pattern, otherwise null.
* @param patternMatrix the pattern matrix, may be overridden for custom rendering.
* @throws IOException if there is an error reading or parsing the tiling pattern content stream.
*/
protected final void processTilingPattern(PDTilingPattern tilingPattern, PDColor color,
PDColorSpace colorSpace, Matrix patternMatrix)
throws IOException
{
PDResources parent = pushResources(tilingPattern);
Matrix parentMatrix = initialMatrix;
initialMatrix = Matrix.concatenate(initialMatrix, patternMatrix);
// save the original graphics state
Stack<PDGraphicsState> savedStack = saveGraphicsStack();
// save a clean state (new clipping path, line path, etc.)
Rectangle2D bbox = tilingPattern.getBBox().transform(patternMatrix).getBounds2D();
PDRectangle rect = new PDRectangle((float)bbox.getX(), (float)bbox.getY(),
(float)bbox.getWidth(), (float)bbox.getHeight());
graphicsStack.push(new PDGraphicsState(rect));
// non-colored patterns have to be given a color
if (colorSpace != null)
{
color = new PDColor(color.getComponents(), colorSpace);
getGraphicsState().setNonStrokingColorSpace(colorSpace);
getGraphicsState().setNonStrokingColor(color);
getGraphicsState().setStrokingColorSpace(colorSpace);
getGraphicsState().setStrokingColor(color);
}
// transform the CTM using the stream's matrix
getGraphicsState().getCurrentTransformationMatrix().concatenate(patternMatrix);
// clip to bounding box
clipToRect(tilingPattern.getBBox());
processStreamOperators(tilingPattern);
initialMatrix = parentMatrix;
restoreGraphicsStack(savedStack);
popResources(parent);
}
/**
* Shows the given annotation.
*
* @param annotation An annotation on the current page.
* @throws IOException If an error occurred reading the annotation
*/
public void showAnnotation(PDAnnotation annotation) throws IOException
{
PDAppearanceStream appearanceStream = getAppearance(annotation);
if (appearanceStream != null)
{
processAnnotation(annotation, appearanceStream);
}
}
/**
* Returns the appearance stream to process for the given annotation. May be used to render
* a specific appearance such as "hover".
*
* @param annotation The current annotation.
* @return The stream to process.
*/
public PDAppearanceStream getAppearance(PDAnnotation annotation)
{
return annotation.getNormalAppearanceStream();
}
/**
* Process a child stream of the given page. Cannot be used with {@link #processPage(PDPage)}.
*
* @param contentStream the child content stream
* @param page
* @throws IOException if there is an exception while processing the stream
*/
protected void processChildStream(PDContentStream contentStream, PDPage page) throws IOException
{
if (isProcessingPage)
{
throw new IllegalStateException("Current page has already been set via " +
" #processPage(PDPage) call #processChildStream(PDContentStream) instead");
}
initPage(page);
processStream(contentStream);
currentPage = null;
}
/**
* Process a content stream.
*
* @param contentStream the content stream
* @throws IOException if there is an exception while processing the stream
*/
private void processStream(PDContentStream contentStream) throws IOException
{
PDResources parent = pushResources(contentStream);
Stack<PDGraphicsState> savedStack = saveGraphicsStack();
Matrix parentMatrix = initialMatrix;
// transform the CTM using the stream's matrix
getGraphicsState().getCurrentTransformationMatrix().concatenate(contentStream.getMatrix());
// the stream's initial matrix includes the parent CTM, e.g. this allows a scaled form
initialMatrix = getGraphicsState().getCurrentTransformationMatrix().clone();
// clip to bounding box
PDRectangle bbox = contentStream.getBBox();
clipToRect(bbox);
processStreamOperators(contentStream);
initialMatrix = parentMatrix;
restoreGraphicsStack(savedStack);
popResources(parent);
}
/**
* Processes the operators of the given content stream.
*
* @param contentStream to content stream to parse.
* @throws IOException if there is an error reading or parsing the content stream.
*/
private void processStreamOperators(PDContentStream contentStream) throws IOException
{
List<COSBase> arguments = new ArrayList<>();
PDFStreamParser parser = new PDFStreamParser(contentStream);
Object token = parser.parseNextToken();
while (token != null)
{
if (token instanceof COSObject)
{
arguments.add(((COSObject) token).getObject());
}
else if (token instanceof Operator)
{
processOperator((Operator) token, arguments);
arguments = new ArrayList<>();
}
else
{
arguments.add((COSBase) token);
}
token = parser.parseNextToken();
}
}
/**
* Pushes the given stream's resources, returning the previous resources.
*/
private PDResources pushResources(PDContentStream contentStream)
{
// resource lookup: first look for stream resources, then fallback to the current page
PDResources parentResources = resources;
PDResources streamResources = contentStream.getResources();
if (streamResources != null)
{
resources = streamResources;
}
else if (resources != null)
{
// inherit directly from parent stream, this is not in the PDF spec, but the file from
// PDFBOX-1359 does this and works in Acrobat
}
else
{
resources = currentPage.getResources();
}
// resources are required in PDF
if (resources == null)
{
resources = new PDResources();
}
return parentResources;
}
/**
* Pops the current resources, replacing them with the given resources.
*/
private void popResources(PDResources parentResources)
{
resources = parentResources;
}
/**
* Transforms the given rectangle using the CTM and then intersects it with the current
* clipping area.
*/
private void clipToRect(PDRectangle rectangle)
{
if (rectangle != null)
{
GeneralPath clip = rectangle.transform(getGraphicsState().getCurrentTransformationMatrix());
getGraphicsState().intersectClippingPath(clip);
}
}
/**
* Called when the BT operator is encountered. This method is for overriding in subclasses, the
* default implementation does nothing.
*
* @throws IOException if there was an error processing the text
*/
public void beginText() throws IOException
{
// overridden in subclasses
}
/**
* Called when the ET operator is encountered. This method is for overriding in subclasses, the
* default implementation does nothing.
*
* @throws IOException if there was an error processing the text
*/
public void endText() throws IOException
{
// overridden in subclasses
}
/**
* Called when a string of text is to be shown.
*
* @param string the encoded text
* @throws IOException if there was an error showing the text
*/
public void showTextString(byte[] string) throws IOException
{
showText(string);
}
/**
* Called when a string of text with spacing adjustments is to be shown.
*
* @param array array of encoded text strings and adjustments
* @throws IOException if there was an error showing the text
*/
public void showTextStrings(COSArray array) throws IOException
{
PDTextState textState = getGraphicsState().getTextState();
float fontSize = textState.getFontSize();
float horizontalScaling = textState.getHorizontalScaling() / 100f;
PDFont font = textState.getFont();
boolean isVertical = false;
if (font != null)
{
isVertical = font.isVertical();
}
for (COSBase obj : array)
{
if (obj instanceof COSNumber)
{
float tj = ((COSNumber)obj).floatValue();
// calculate the combined displacements
float tx, ty;
if (isVertical)
{
tx = 0;
ty = -tj / 1000 * fontSize;
}
else
{
tx = -tj / 1000 * fontSize * horizontalScaling;
ty = 0;
}
applyTextAdjustment(tx, ty);
}
else if(obj instanceof COSString)
{
byte[] string = ((COSString)obj).getBytes();
showText(string);
}
else
{
throw new IOException("Unknown type in array for TJ operation:" + obj);
}
}
}
/**
* Applies a text position adjustment from the TJ operator. May be overridden in subclasses.
*
* @param tx x-translation
* @param ty y-translation
*/
protected void applyTextAdjustment(float tx, float ty) throws IOException
{
// update the text matrix
textMatrix.concatenate(Matrix.getTranslateInstance(tx, ty));
}
/**
* Process text from the PDF Stream. You should override this method if you want to
* perform an action when encoded text is being processed.
*
* @param string the encoded text
* @throws IOException if there is an error processing the string
*/
protected void showText(byte[] string) throws IOException
{
PDGraphicsState state = getGraphicsState();
PDTextState textState = state.getTextState();
// get the current font
PDFont font = textState.getFont();
if (font == null)
{
LOG.warn("No current font, will use default");
font = PDFontFactory.createDefaultFont();
}
float fontSize = textState.getFontSize();
float horizontalScaling = textState.getHorizontalScaling() / 100f;
float charSpacing = textState.getCharacterSpacing();
// put the text state parameters into matrix form
Matrix parameters = new Matrix(
fontSize * horizontalScaling, 0, // 0
0, fontSize, // 0
0, textState.getRise()); // 1
// read the stream until it is empty
InputStream in = new ByteArrayInputStream(string);
while (in.available() > 0)
{
// decode a character
int before = in.available();
int code = font.readCode(in);
int codeLength = before - in.available();
String unicode = font.toUnicode(code);
// Word spacing shall be applied to every occurrence of the single-byte character code
// 32 in a string when using a simple font or a composite font that defines code 32 as
// a single-byte code.
float wordSpacing = 0;
if (codeLength == 1 && code == 32)
{
wordSpacing += textState.getWordSpacing();
}
// text rendering matrix (text space -> device space)
Matrix ctm = state.getCurrentTransformationMatrix();
Matrix textRenderingMatrix = parameters.multiply(textMatrix).multiply(ctm);
// get glyph's position vector if this is vertical text
// changes to vertical text should be tested with PDFBOX-2294 and PDFBOX-1422
if (font.isVertical())
{
// position vector, in text space
Vector v = font.getPositionVector(code);
// apply the position vector to the horizontal origin to get the vertical origin
textRenderingMatrix.translate(v);
}
// get glyph's horizontal and vertical displacements, in text space
Vector w = font.getDisplacement(code);
// process the decoded glyph
saveGraphicsState();
Matrix textMatrixOld = textMatrix;
Matrix textLineMatrixOld = textLineMatrix;
showGlyph(textRenderingMatrix, font, code, unicode, w);
textMatrix = textMatrixOld;
textLineMatrix = textLineMatrixOld;
restoreGraphicsState();
// calculate the combined displacements
float tx, ty;
if (font.isVertical())
{
tx = 0;
ty = w.getY() * fontSize + charSpacing + wordSpacing;
}
else
{
tx = (w.getX() * fontSize + charSpacing + wordSpacing) * horizontalScaling;
ty = 0;
}
// update the text matrix
textMatrix.concatenate(Matrix.getTranslateInstance(tx, ty));
}
}
/**
* Called when a glyph is to be processed.This method is intended for overriding in subclasses,
* the default implementation does nothing.
*
* @param textRenderingMatrix the current text rendering matrix, T<sub>rm</sub>
* @param font the current font
* @param code internal PDF character code for the glyph
* @param unicode the Unicode text for this glyph, or null if the PDF does provide it
* @param displacement the displacement (i.e. advance) of the glyph in text space
* @throws IOException if the glyph cannot be processed
*/
protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode,
Vector displacement) throws IOException
{
if (font instanceof PDType3Font)
{
showType3Glyph(textRenderingMatrix, (PDType3Font)font, code, unicode, displacement);
}
else
{
showFontGlyph(textRenderingMatrix, font, code, unicode, displacement);
}
}
/**
* Called when a glyph is to be processed.This method is intended for overriding in subclasses,
* the default implementation does nothing.
*
* @param textRenderingMatrix the current text rendering matrix, T<sub>rm</sub>
* @param font the current font
* @param code internal PDF character code for the glyph
* @param unicode the Unicode text for this glyph, or null if the PDF does provide it
* @param displacement the displacement (i.e. advance) of the glyph in text space
* @throws IOException if the glyph cannot be processed
*/
protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode,
Vector displacement) throws IOException
{
// overridden in subclasses
}
/**
* Called when a glyph is to be processed.This method is intended for overriding in subclasses,
* the default implementation does nothing.
*
* @param textRenderingMatrix the current text rendering matrix, T<sub>rm</sub>
* @param font the current font
* @param code internal PDF character code for the glyph
* @param unicode the Unicode text for this glyph, or null if the PDF does provide it
* @param displacement the displacement (i.e. advance) of the glyph in text space
* @throws IOException if the glyph cannot be processed
*/
protected void showType3Glyph(Matrix textRenderingMatrix, PDType3Font font, int code,
String unicode, Vector displacement) throws IOException
{
PDType3CharProc charProc = font.getCharProc(code);
if (charProc != null)
{
processType3Stream(charProc, textRenderingMatrix);
}
}
/**
* This is used to handle an operation.
*
* @param operation The operation to perform.
* @param arguments The list of arguments.
* @throws IOException If there is an error processing the operation.
*/
public void processOperator(String operation, List<COSBase> arguments) throws IOException
{
Operator operator = Operator.getOperator(operation);
processOperator(operator, arguments);
}
/**
* This is used to handle an operation.
*
* @param operator The operation to perform.
* @param operands The list of arguments.
* @throws IOException If there is an error processing the operation.
*/
protected void processOperator(Operator operator, List<COSBase> operands) throws IOException
{
String name = operator.getName();
OperatorProcessor processor = operators.get(name);
if (processor != null)
{
processor.setContext(this);
try
{
processor.process(operator, operands);
}
catch (IOException e)
{
operatorException(operator, operands, e);
}
}
else
{
unsupportedOperator(operator, operands);
}
}
/**
* Called when an unsupported operator is encountered.
*
* @param operator The unknown operator.
* @param operands The list of operands.
*/
protected void unsupportedOperator(Operator operator, List<COSBase> operands) throws IOException
{
// overridden in subclasses
}
/**
* Called when an exception is thrown by an operator.
*
* @param operator The unknown operator.
* @param operands The list of operands.
*/
protected void operatorException(Operator operator, List<COSBase> operands, IOException e)
throws IOException
{
if (e instanceof MissingOperandException ||
e instanceof MissingResourceException ||
e instanceof MissingImageReaderException)
{
LOG.error(e.getMessage());
}
else if (e instanceof EmptyGraphicsStackException)
{
LOG.warn(e.getMessage());
}
else if (operator.getName().equals("Do"))
{
// todo: this too forgiving, but PDFBox has always worked this way for DrawObject
// some careful refactoring is needed
LOG.warn(e.getMessage());
}
else
{
throw e;
}
}
/**
* Pushes the current graphics state to the stack.
*/
public void saveGraphicsState()
{
graphicsStack.push(graphicsStack.peek().clone());
}
/**
* Pops the current graphics state from the stack.
*/
public void restoreGraphicsState()
{
graphicsStack.pop();
}
/**
* Saves the entire graphics stack.
*/
protected final Stack<PDGraphicsState> saveGraphicsStack()
{
Stack<PDGraphicsState> savedStack = graphicsStack;
graphicsStack = new Stack<>();
graphicsStack.add(savedStack.peek().clone());
return savedStack;
}
/**
* Restores the entire graphics stack.
*/
protected final void restoreGraphicsStack(Stack<PDGraphicsState> snapshot)
{
graphicsStack = snapshot;
}
/**
* @return Returns the size of the graphicsStack.
*/
public int getGraphicsStackSize()
{
return graphicsStack.size();
}
/**
* @return Returns the graphicsState.
*/
public PDGraphicsState getGraphicsState()
{
return graphicsStack.peek();
}
/**
* @return Returns the textLineMatrix.
*/
public Matrix getTextLineMatrix()
{
return textLineMatrix;
}
/**
* @param value The textLineMatrix to set.
*/
public void setTextLineMatrix(Matrix value)
{
textLineMatrix = value;
}
/**
* @return Returns the textMatrix.
*/
public Matrix getTextMatrix()
{
return textMatrix;
}
/**
* @param value The textMatrix to set.
*/
public void setTextMatrix(Matrix value)
{
textMatrix = value;
}
/**
* @param array dash array
* @param phase dash phase
*/
public void setLineDashPattern(COSArray array, int phase)
{
if (phase < 0)
{
LOG.warn("Dash phase has negative value " + phase + ", set to 0");
phase = 0;
}
PDLineDashPattern lineDash = new PDLineDashPattern(array, phase);
getGraphicsState().setLineDashPattern(lineDash);
}
/**
* Returns the stream' resources.
*/
public PDResources getResources()
{
return resources;
}
/**
* Returns the current page.
*/
public PDPage getCurrentPage()
{
return currentPage;
}
/**
* Gets the stream's initial matrix.
*/
public Matrix getInitialMatrix()
{
return initialMatrix;
}
/**
* Transforms a point using the CTM.
*/
public Point2D.Float transformedPoint(float x, float y)
{
float[] position = { x, y };
getGraphicsState().getCurrentTransformationMatrix().createAffineTransform()
.transform(position, 0, position, 0, 1);
return new Point2D.Float(position[0], position[1]);
}
/**
* Transforms a width using the CTM.
*/
protected float transformWidth(float width)
{
Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
float x = ctm.getScaleX() + ctm.getShearX();
float y = ctm.getScaleY() + ctm.getShearY();
return width * (float)Math.sqrt((x * x + y * y) * 0.5);
}
}