/*
* 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.
*/
/* $Id: PDFState.java 557337 2007-07-18 17:37:14Z adelmelle $ */
package org.apache.fop.pdf;
import java.io.Serializable;
import java.util.List;
import java.util.Iterator;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
/**
* This keeps information about the current state when writing to pdf.
* It allows for creating new graphics states with the q operator.
* This class is only used to store the information about the state
* the caller needs to handle the actual pdf operators.
*
* When setting the state for pdf there are three possible ways of
* handling the situation.
* The values can be set to override previous or default values.
* A new state can be added and then the values set.
* The current state can be popped and values will return to a
* previous state then the necessary values can be overridden.
* The current transform behaves differently to other values as the
* matrix is combined with the current resolved value.
* It is impossible to optimise the result without analysing the all
* the possible combinations after completing.
*/
public class PDFState {
private Data data = new Data();
private List stateStack = new java.util.ArrayList();
/**
* PDF State for storing graphics state.
*/
public PDFState() {
}
/**
* Push the current state onto the stack.
* This call should be used when the q operator is used
* so that the state is known when popped.
*/
public void push() {
Data copy;
try {
copy = (Data)getData().clone();
getData().resetTransform();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e.getMessage());
}
stateStack.add(copy);
}
/**
* @return the currently valid state
*/
public Data getData() {
return data;
}
/**
* Pop the state from the stack and set current values to popped state.
* This should be called when a Q operator is used so
* the state is restored to the correct values.
* @return the restored state, null if the stack is empty
*/
public Data pop() {
if (getStackLevel() > 0) {
Data popped = (Data)stateStack.remove(stateStack.size() - 1);
data = popped;
return popped;
} else {
return null;
}
}
/**
* Get the current stack level.
*
* @return the current stack level
*/
public int getStackLevel() {
return stateStack.size();
}
/**
* Restore the state to a particular level.
* this can be used to restore to a known level without making
* multiple pop calls.
*
* @param stack the level to restore to
*/
/*
public void restoreLevel(int stack) {
int pos = stack;
while (stateStack.size() > pos + 1) {
stateStack.remove(stateStack.size() - 1);
}
if (stateStack.size() > pos) {
pop();
}
}*/
/**
* Set the current line dash.
* Check if setting the line dash to the given values
* will make a change and then set the state to the new values.
*
* @param array the line dash array
* @param offset the line dash start offset
* @return true if the line dash has changed
*/
/*
public boolean setLineDash(int[] array, int offset) {
return false;
}*/
/**
* Set the current line width.
* @param width the line width in points
* @return true if the line width has changed
*/
public boolean setLineWidth(float width) {
if (getData().lineWidth != width) {
getData().lineWidth = width;
return true;
} else {
return false;
}
}
/**
* Set the current color.
* Check if the new color is a change and then set the current color.
*
* @param col the color to set
* @return true if the color has changed
*/
public boolean setColor(Color col) {
if (!col.equals(getData().color)) {
getData().color = col;
return true;
} else {
return false;
}
}
/**
* Set the current background color.
* Check if the background color will change and then set the new color.
*
* @param col the new background color
* @return true if the background color has changed
*/
public boolean setBackColor(Color col) {
if (!col.equals(getData().backcolor)) {
getData().backcolor = col;
return true;
} else {
return false;
}
}
/**
* Set the current paint.
* This checks if the paint will change and then sets the current paint.
*
* @param p the new paint
* @return true if the new paint changes the current paint
*/
public boolean setPaint(Paint p) {
if (getData().paint == null) {
if (p != null) {
getData().paint = p;
return true;
}
} else if (!data.paint.equals(p)) {
getData().paint = p;
return true;
}
return false;
}
/**
* Check if the clip will change the current state.
* A clip is assumed to be used in a situation where it will add
* to any clip in the current or parent states.
* A clip cannot be cleared, this can only be achieved by going to
* a parent level with the correct clip.
* If the clip is different then it may start a new state so that
* it can return to the previous clip.
*
* @param cl the clip shape to check
* @return true if the clip will change the current clip.
*/
public boolean checkClip(Shape cl) {
if (getData().clip == null) {
if (cl != null) {
return true;
}
} else if (!new Area(getData().clip).equals(new Area(cl))) {
return true;
}
//TODO check for clips that are larger than the current
return false;
}
/**
* Set the current clip.
* This either sets a new clip or sets the clip to the intersect of
* the old clip and the new clip.
*
* @param cl the new clip in the current state
*/
public void setClip(Shape cl) {
if (getData().clip != null) {
Area newClip = new Area(getData().clip);
newClip.intersect(new Area(cl));
getData().clip = new GeneralPath(newClip);
} else {
getData().clip = cl;
}
}
/**
* Check the current transform.
* The transform for the current state is the combination of all
* transforms in the current state. The parameter is compared
* against this current transform.
*
* @param tf the transform the check against
* @return true if the new transform is different then the current transform
*/
public boolean checkTransform(AffineTransform tf) {
return !tf.equals(getData().transform);
}
/**
* Set a new transform.
* This transform is appended to the transform of
* the current graphic state.
*
* @param tf the transform to concatonate to the current level transform
* @deprecated This method name is misleading. Use concatenate(AffineTransform) instead!
*/
public void setTransform(AffineTransform tf) {
concatenate(tf);
}
/**
* Concatenates the given AffineTransform to the current one.
* @param tf the transform to concatenate to the current level transform
*/
public void concatenate(AffineTransform tf) {
getData().concatenate(tf);
}
/**
* Get the current transform.
* This gets the combination of all transforms in the
* current state.
*
* @return the calculate combined transform for the current state
*/
public AffineTransform getTransform() {
AffineTransform tf;
AffineTransform at = new AffineTransform();
for (Iterator iter = stateStack.iterator(); iter.hasNext();) {
Data d = (Data)iter.next();
tf = d.transform;
at.concatenate(tf);
}
at.concatenate(getData().transform);
return at;
}
/**
* Get a copy of the base transform for the page. Used to translate
* IPP/BPP values into X,Y positions when positioning is "fixed".
*
* @return the base transform, or null if the state stack is empty
*/
public AffineTransform getBaseTransform() {
if (stateStack.size() == 0) {
return null;
} else {
Data baseData = (Data) stateStack.get(0);
return (AffineTransform) baseData.transform.clone();
}
}
/**
* Get the graphics state.
* This gets the combination of all graphic states for
* the current context.
* This is the graphic state set with the gs operator not
* the other graphic state changes.
*
* @return the calculated ExtGState in the current context
*/
public PDFGState getGState() {
PDFGState defaultState = PDFGState.DEFAULT;
PDFGState state;
PDFGState newstate = new PDFGState();
newstate.addValues(defaultState);
for (Iterator iter = stateStack.iterator(); iter.hasNext();) {
Data d = (Data)iter.next();
state = d.gstate;
if (state != null) {
newstate.addValues(state);
}
}
if (getData().gstate != null) {
newstate.addValues(getData().gstate);
}
return newstate;
}
public class Data implements Cloneable, Serializable {
public Color color = Color.black;
public Color backcolor = Color.black;
public Paint paint = null;
public Paint backPaint = null;
public int lineCap = 0;
public int lineJoin = 0;
public float lineWidth = 1;
public float miterLimit = 0;
public boolean text = false;
public int dashOffset = 0;
public int[] dashArray = new int[0];
public AffineTransform transform = new AffineTransform();
public float fontSize = 0;
public String fontName = "";
public Shape clip = null;
public PDFGState gstate = null;
/** {@inheritDoc} */
public Object clone() throws CloneNotSupportedException {
Data obj = new Data();
obj.color = this.color;
obj.backcolor = this.backcolor;
obj.paint = this.paint;
obj.backPaint = this.paint;
obj.lineCap = this.lineCap;
obj.lineJoin = this.lineJoin;
obj.lineWidth = this.lineWidth;
obj.miterLimit = this.miterLimit;
obj.text = this.text;
obj.dashOffset = this.dashOffset;
obj.dashArray = this.dashArray;
obj.transform = new AffineTransform(this.transform);
obj.fontSize = this.fontSize;
obj.fontName = this.fontName;
obj.clip = this.clip;
obj.gstate = this.gstate;
return obj;
}
/**
* Get the current Transform.
*/
public AffineTransform getTransform() {
return transform;
}
public void resetTransform() {
transform = new AffineTransform();
}
/**
* Concatenate the given AffineTransform with the current thus creating
* a new viewport. Note that all concatenation operations are logged
* so they can be replayed if necessary (ex. for block-containers with
* "fixed" positioning.
* @param at Transformation to perform
*/
public void concatenate(AffineTransform at) {
transform.concatenate(at);
}
/** {@inheritDoc} */
public String toString() {
return super.toString() + ", " + this.transform;
}
}
}