/* * Copyright 2006-2017 ICEsoft Technologies Canada Corp. * * Licensed 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.icepdf.core.pobjects.graphics; import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.graphics.commands.*; import org.icepdf.core.util.Defs; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * <p>The PDF viewer application maintains an internal data structure called the * graphics state that holds current graphics control parameters. These * parameters define the global framework within which the graphics operators * execute.</p> * <br> * <p>The graphics state is initialized at the beginning of each page, using the * default values specified in Tables 4.2 and 4.3. Table 4.2 lists those * graphics state parameters that are device-independent and are appropriate * to specify in page descriptions. The parameters listed in Table 4.3 control * details of the rendering (scan conversion) process and are device-dependent; * a page description that is intended to be device-independent should not * modify these parameters.</p> * <br> * <h2>Graphics State Stack Info</h2> * <p>A well-structured PDF document typically contains many graphical elements * that are essentially independent of each other and sometimes nested to * multiple levels. The graphics state stack allows these elements to make local * changes to the graphics state without disturbing the graphics state of the * surrounding environment. The stack is a LIFO (last in, first out) data * structure in which the contents of the graphics state can be saved and later * restored using the following operators: * <ul> * <li><p>The q operator pushes a copy of the entire graphics state onto the * stack. This is handled by the save() method in this class</li> * <li><p>The Q operator restores the entire graphics state to its former * value by popping it from the stack. This is handled by the * restore() method in this class</p><li> * </ul> * <p>When a graphics state is saved a new GraphicsState object is created and * a pointer is set to the origional graphics state. This creates a linked * list of graphics states that can be easily restored when needed (LIFO staack) * </p> * <h2>Shapes Stack</h2> * <p>The graphics state also manipulates the Shapes stack that contains all of * the rendering components dealing with graphics states. As the content parser * encounters different graphic state manipulators they are added to the stack * and then when the page is rendered the stack (actually a vector) is read * in a FIFO to generate the drawing commands.</p> * <br> * <h2>Device-independent graphics state parameters - (Table 4.2)</h2> * <table border="1" cellpadding="1" cellspacing="1" summary=""> * <tr> * <td><b> Paramater </b></td> * <td><b> Type</b></td> * <td><b> Value</b></td> * </tr> * <tr> * <td valign="top" >CTM</td> * <td valign="top" >array</td> * <td>The current transformation matrix, which maps positions from user * coordinates to device coordinates. This matrix is modified by each * application of the coordinate transformation operator, cm. Initial * value: a matrix that transforms default user coordinates to device * coordinates.</td> * </tr> * <tr> * <td valign="top" >clipping path</td> * <td valign="top" >(internal)</td> * <td>The current clipping path, which defines the boundary against which * all output is to be cropped. Initial value: the boundary of the * entire imageable portion of the output page.</td> * </tr> * <tr> * <td valign="top" >color space</td> * <td valign="top" >name or array</td> * <td>The current color space in which color values are to be interpreted. * There are two separate color space parameters: one for stroking and * one for all other painting operations. Initial value: DeviceGray.</td> * </tr> * <tr> * <td valign="top" >color</td> * <td valign="top" >(various</td> * <td>The current color to be used during painting operations. The type * and interpretation of this parameter depend on the current color * space; for most color spaces, a color value consists of one to four * numbers. There are two separate color parameters: one for stroking * and one for all other painting operations. Initial value: black.</td> * </tr> * <tr> * <td valign="top" >text state</td> * <td valign="top" >(various)</td> * <td>A set of nine graphics state parameters that pertain only to the * painting of text. These include parameters that select the font, * scale the glyphs to an appropriate size, and accomplish other effects. * The text state parameters are described in Section 5.2, "Text State * Parameters and Operators."</td> * </tr> * <tr> * <td valign="top" >line width</td> * <td valign="top" >number</td> * <td>The thickness, in user space units, of paths to be stroked.</td> * </tr> * <tr> * <td valign="top" >line cap</td> * <td valign="top" >integer</td> * <td>A code specifying the shape of the endpoints for any open path that * is stroked (see "Line Cap Style" on page 186). Initial value: 0, for * square butt caps.</td> * </tr> * <tr> * <td valign="top" >line join</td> * <td valign="top" >integer</td> * <td>A code specifying the shape of joints between connected segments of * a stroked path. Initial value: 0, for mitered joins.</td> * </tr> * <tr> * <td valign="top" >miter limit</td> * <td valign="top" >number</td> * <td>The maximum length of mitered line joins for stroked paths. This * parameter limits the length of "spikes" produced when line segments * join at sharp angles. Initial value: 10.0, for a miter cutoff below * approximately 11.5 degrees.</td> * </tr> * <tr> * <td valign="top" >dash pattern</td> * <td valign="top" >array and number</td> * <td>A description of the dash pattern to be used when paths are stroked. * Initial value: a solid line.</td> * </tr> * <tr> * <td valign="top" >rendering intent (Not supported)</td> * <td valign="top" >name</td> * <td>The rendering intent to be used when converting CIE-based colors to * device colors. Default value: RelativeColorimetric.</td> * </tr> * <tr> * <td valign="top" >stroke adjustment (Not supported)</td> * <td valign="top" >boolean</td> * <td>(PDF 1.2) A flag specifying whether to compensate for possible * rasterization effects when stroking a path with a line width that is * small relative to the pixel resolution of the output device.. * Note that this is considered a device-independent parameter, even * though the details of its effects are device-dependent. * Initial value: false.</td> * </tr> * <tr> * <td valign="top" >blend mode (Not supported)</td> * <td valign="top" >name or array</td> * <td>(PDF 1.4) The current blend mode to be used in the transparent * imaging model. This parameter is implicitly reset to its initial * value at the beginning of execution of a transparency group XObject. * Initial value: Normal.</td> * </tr> * <tr> * <td valign="top" >soft mask (Not supported)</td> * <td valign="top" >dictionary or name</td> * <td>(PDF 1.4) A soft-mask dictionary specifying the mask shape or mask * opacity values to be used in the transparent imaging model, or the * name None if no such mask is specified. This parameter is implicitly * reset to its initial value at the beginning of execution of a * transparency group XObject. Initial value: None.</td> * </tr> * <tr> * <td valign="top" >alpha constant</td> * <td valign="top" >number</td> * <td>(PDF 1.4) The constant shape or constant opacity value to be used in * the transparent imaging model. There are two separate alpha constant * parameters: one for stroking and one for all other painting * operations. This parameter is implicitly reset to its initial value * at the beginning of execution of a transparency group XObject. * Initial value: 1.0.</td> * </tr> * <tr> * <td valign="top" >alpha source</td> * <td valign="top" >boolean</td> * <td>(PDF 1.4) A flag specifying whether the current soft mask and alpha * constant parameters are to be interpreted as shape values (true) or * opacity values (false). This flag also governs the interpretation of * the SMask entry, if any, in an image dictionary. * Initial value: false.</td> * </tr> * </table> * <br> * <h2>Device-Dependent graphics state parameters - (Table 4.3) </h2> * <p><b>Currently Not supported</b></p> * * @since 1.0 */ public class GraphicsState { private static final Logger logger = Logger.getLogger(GraphicsState.class.toString()); public static final Name CA_STROKING_KEY = new Name("CA"); public static final Name CA_NON_STROKING_KEY = new Name("ca"); // allow over paint support for fill and stroke. Still experimental // enabled buy default but can be turned off if required. private static boolean enabledOverpaint; static { enabledOverpaint = Defs.sysPropertyBoolean("org.icepdf.core.overpaint", true); } // Current transformation matrix. private AffineTransform CTM; private static ClipDrawCmd clipDrawCmd = new ClipDrawCmd(); private static NoClipDrawCmd noClipDrawCmd = new NoClipDrawCmd(); // Specifies the shape of the endpoint for any open path. private int lineCap; // Specifies the thickness in user parse of a path to be stroked. private float lineWidth; // Specifies the shape of the joints between connected segments. private int lineJoin; // Maximum length of mitered line join for stroked paths. private float miterLimit; // Stores the lengths of the dash segments private float[] dashArray; // Stores the current dash phase private float dashPhase; // color used to fill a stroked path. private Color fillColor; // The current color to be used during a painting operation. private Color strokeColor; // Stroking alpha constant for "CA" private float strokeAlpha; // Fill alpha constant for "ca" or non-stroking alpha private float fillAlpha; // Transparency grouping changes which transparency rule is applied. // Normally it is simply a SRC_OVER rule but the group can also have isolated // and knockout values that directly affect which rule is used for the // transparency. private ExtGState extGState; private int alphaRule; private boolean transparencyGroup; private boolean isolated; private boolean knockOut; // Color space of the fill color, non-stroking colour. private PColorSpace fillColorSpace; // Color space of the stroke color, stroking colour. private PColorSpace strokeColorSpace; // Set of graphics stat parameter for painting text. private TextState textState; // parent graphics state if it exists. private GraphicsState parentGraphicState; // all shapes associated with this graphics state. private Shapes shapes; // current clipping area. private Area clip; private boolean clipChange; // over print mode private int overprintMode; // over printing stroking private boolean overprintStroking; // over printing everything other than stroking private boolean overprintOther; /** * Constructs a new <code>GraphicsState</code> which will have default * values and shapes specified by the shapes stack. * * @param shapes stack containing pages graphical elements. */ public GraphicsState(Shapes shapes) { this.shapes = shapes; CTM = new AffineTransform(); lineCap = BasicStroke.CAP_BUTT; lineWidth = 1; lineJoin = BasicStroke.JOIN_MITER; miterLimit = 10; fillColor = Color.black; strokeColor = Color.black; strokeAlpha = 1.0f; fillAlpha = 1.0f; alphaRule = AlphaComposite.SRC_OVER; fillColorSpace = new DeviceGray(null, null); strokeColorSpace = new DeviceGray(null, null); textState = new TextState(); } /** * Constructs a new <code>GraphicsState</code> that is a copy of * the specified <code>GraphicsState</code> object. * * @param parentGraphicsState the <code>GraphicsState</code> object to copy */ public GraphicsState(GraphicsState parentGraphicsState) { // copy/clone the parentGraphicsState and return the new object. CTM = new AffineTransform(parentGraphicsState.CTM); lineCap = parentGraphicsState.lineCap; lineWidth = parentGraphicsState.lineWidth; miterLimit = parentGraphicsState.miterLimit; lineJoin = parentGraphicsState.lineJoin; fillColor = new Color(parentGraphicsState.fillColor.getRGB(), true); strokeColor = new Color(parentGraphicsState.strokeColor.getRGB(), true); shapes = parentGraphicsState.shapes; if (parentGraphicsState.clip != null) { clip = (Area) parentGraphicsState.clip.clone(); } fillColorSpace = parentGraphicsState.fillColorSpace; strokeColorSpace = parentGraphicsState.strokeColorSpace; textState = new TextState(parentGraphicsState.textState); dashPhase = parentGraphicsState.dashPhase; dashArray = parentGraphicsState.dashArray; // copy over printing attributes overprintMode = parentGraphicsState.overprintMode; overprintOther = parentGraphicsState.overprintOther; overprintStroking = parentGraphicsState.overprintStroking; // copy the alpha rules fillAlpha = parentGraphicsState.fillAlpha; strokeAlpha = parentGraphicsState.strokeAlpha; alphaRule = parentGraphicsState.alphaRule; // extra graphics if (parentGraphicsState.getExtGState() != null) { extGState = new ExtGState(parentGraphicsState.getExtGState().getLibrary(), parentGraphicsState.getExtGState().getEntries()); } // copy the parent too. this.parentGraphicState = parentGraphicsState.parentGraphicState; } /** * Sets the Shapes vector. * * @param shapes shapes for a given content stream. */ public void setShapes(Shapes shapes) { this.shapes = shapes; } /** * Concatenates this transform with a translation transformation specified * by the graphics state current CTM. An updated CTM is added to the * shapes stack. * * @param x the distance by which coordinates are translated in the * X axis direction * @param y the distance by which coordinates are translated in the * Y axis direction */ public void translate(double x, double y) { CTM.translate(x, y); shapes.add(new TransformDrawCmd(new AffineTransform(CTM))); } /** * Concatenates this transform with a scaling transformation specified * by the graphics state current CTM. An update CTM is added to the * shapes stack. * * @param x the factor by which coordinates are scaled along the * X axis direction * @param y the factor by which coordinates are scaled along the * Y axis direction */ public void scale(double x, double y) { CTM.scale(x, y); shapes.add(new TransformDrawCmd(new AffineTransform(CTM))); } /** * Sets the graphics state CTM to a new transform, the old CTM transform is * lost. The new CTM value is added to the shapes stack. * * @param af the AffineTranform object to set the CTM to. */ public void set(AffineTransform af) { // appling a CTM can be expensive, so only do it if it's needed. if (!CTM.equals(af)) { CTM = new AffineTransform(af); } shapes.add(new TransformDrawCmd(new AffineTransform(CTM))); } /** * Saves the current graphics state. * * @return copy of the current graphics state. * @see #restore() */ public GraphicsState save() { GraphicsState gs = new GraphicsState(this); gs.parentGraphicState = this; return gs; } /** * Concatenate the specified ExtGState to the current graphics state. * <b>Note: </b> currently only a few of the ExtGState attributes are * supported. * * @param extGState external graphics state. * @see org.icepdf.core.pobjects.graphics.ExtGState */ public void concatenate(ExtGState extGState) { // keep a reference for our partial Transparency group support. this.extGState = new ExtGState(extGState.getLibrary(), extGState.getEntries()); // Map over extGState attributes if present. // line width if (extGState.getLineWidth() != null) { setLineWidth(extGState.getLineWidth().floatValue()); } // line cap style if (extGState.getLineCapStyle() != null) { setLineCap(extGState.getLineCapStyle().intValue()); } // line join style if (extGState.getLineJoinStyle() != null) { setLineJoin(extGState.getLineJoinStyle().intValue()); } // line miter limit if (extGState.getMiterLimit() != null) { setMiterLimit(extGState.getMiterLimit().floatValue()); } // line dash pattern if (extGState.getLineDashPattern() != null) { List dasshPattern = extGState.getLineDashPattern(); try { setDashArray((float[]) dasshPattern.get(0)); setDashPhase(((Number) dasshPattern.get(1)).floatValue()); } catch (ClassCastException e) { logger.log(Level.FINE, "Dash cast error: ", e); } } // Stroking alpha if (extGState.getNonStrokingAlphConstant() != -1) { setFillAlpha(extGState.getNonStrokingAlphConstant()); } // none stroking alpha if (extGState.getStrokingAlphConstant() != -1) { setStrokeAlpha(extGState.getStrokingAlphConstant()); } setOverprintMode(extGState.getOverprintMode()); // apply over print logic processOverPaint(extGState.getOverprint(), extGState.getOverprintFill()); } /** * Process the OP and op over printing attributes. * * @param OP OP graphics state param * @param op op graphics state param */ private void processOverPaint(Boolean OP, Boolean op) { // PDF 1.2 and earlier, only a single overprint parameter if (enabledOverpaint && OP != null && op == null && overprintMode == 1) { overprintStroking = OP; overprintOther = overprintStroking; } // PDF 1.2 and over // else if (OP != null) { // overprintStroking = OP.booleanValue(); // overprintOther = op.booleanValue(); // } // else default inits of false for each is fine. } /** * Restores the previously saved graphic state. * * @return the last saved graphics state. * @see #save() */ public GraphicsState restore() { // make sure we have a parent to restore to if (parentGraphicState != null) { // Add the parents CTM to the stack, parentGraphicState.set(parentGraphicState.CTM); // Add the parents clip to the stack if (clipChange) { if (parentGraphicState.clip != null) { if (!parentGraphicState.clip.equals(clip)) { parentGraphicState.shapes.add(new ShapeDrawCmd(new Area(parentGraphicState.clip))); parentGraphicState.shapes.add(clipDrawCmd); } } else { parentGraphicState.shapes.add(noClipDrawCmd); } } // Update the stack with the parentGraphicsState stack. parentGraphicState.shapes.add(new StrokeDrawCmd( new BasicStroke(parentGraphicState.lineWidth, parentGraphicState.lineCap, parentGraphicState.lineJoin, parentGraphicState.miterLimit, parentGraphicState.dashArray, parentGraphicState.dashPhase))); // Note the following aren't officially part of the graphic state parameters // but they need to be restored in order to show some PDF content correctly // restore the fill color of the last paint parentGraphicState.shapes.add(new ColorDrawCmd(parentGraphicState.getFillColor())); // apply the old alpha fill, removed as we need to guarantee the stack is in the correct state. parentGraphicState.shapes.add(new AlphaDrawCmd( AlphaComposite.getInstance(parentGraphicState.getAlphaRule(), parentGraphicState.getFillAlpha()))); parentGraphicState.shapes.add(new AlphaDrawCmd( AlphaComposite.getInstance(parentGraphicState.getAlphaRule(), parentGraphicState.getStrokeAlpha()))); // stroke Color // parentGraphicState.shapes.add(parentGraphicState.getStrokeColor()); } return parentGraphicState; } /** * Updates the clip every time the tranformation matrix is updated. This is * needed to insure that when a new shape is interesected with the * previous clip that they are both in the same space. * * @param af transform to be applied to the clip. */ public void updateClipCM(AffineTransform af) { // we need to update the current clip with the new transform, which is // actually the inverse of the matrix defined by "cm" in the content // parser. if (clip != null) { // get the inverse of the transform and apply it to clipCM AffineTransform afInverse = new AffineTransform(); try { afInverse = af.createInverse(); } catch (Exception e) { logger.log(Level.FINER, "Error generating clip inverse.", e); } // transform the clip. clip.transform(afInverse); } } /** * Set the graphics state clipping area. The new clipping area is * intersected with the current current clip to generate the new clipping * area which is saved to the stack. * * @param newClip new clip for graphic state. */ public void setClip(Shape newClip) { if (newClip != null) { // intersect can only be calculated on a an area. Area area = new Area(newClip); // make sure the clip is not null if (clip != null) { area.intersect(clip); } // update the clip with the new value if it is new. if (clip == null || !clip.equals(area)) { clip = new Area(area); shapes.add(new ShapeDrawCmd(area)); shapes.add(clipDrawCmd); clipChange = true; if (parentGraphicState != null) parentGraphicState.clipChange = true; } else { clip = new Area(area); } } else { // add a null clip for a null shape, should not normally happen clip = null; shapes.add(noClipDrawCmd); clipChange = true; if (parentGraphicState != null) parentGraphicState.clipChange = true; } } public Area getClip() { return clip; } public AffineTransform getCTM() { return CTM; } public void setCTM(AffineTransform ctm) { CTM = ctm; } public int getLineCap() { return lineCap; } public void setLineCap(int lineCap) { this.lineCap = lineCap; } public float getLineWidth() { return lineWidth; } public void setLineWidth(float lineWidth) { // Automatic Stroke Adjustment if (lineWidth <= Float.MIN_VALUE || lineWidth >= Float.MAX_VALUE || lineWidth == 0) { // set line width to a very small none zero number. this.lineWidth = 0.001f; } else { this.lineWidth = lineWidth; } } public int getLineJoin() { return lineJoin; } public void setLineJoin(int lineJoin) { this.lineJoin = lineJoin; } public float[] getDashArray() { return dashArray; } public void setDashArray(float[] dashArray) { this.dashArray = dashArray; } public float getDashPhase() { return dashPhase; } public void setDashPhase(float dashPhase) { this.dashPhase = dashPhase; } public float getMiterLimit() { return miterLimit; } public void setMiterLimit(float miterLimit) { this.miterLimit = miterLimit; } public Color getFillColor() { return fillColor; } public void setFillColor(Color fillColor) { this.fillColor = fillColor; } public Color getStrokeColor() { return strokeColor; } public void setStrokeAlpha(float alpha) { if (alpha > 1.0f) { alpha = 1.0f; } strokeAlpha = alpha; } public float getStrokeAlpha() { return strokeAlpha; } public void setFillAlpha(float alpha) { if (alpha > 1.0f) { alpha = 1.0f; } fillAlpha = alpha; } public float getFillAlpha() { return fillAlpha; } public void setStrokeColor(Color strokeColor) { this.strokeColor = strokeColor; } public PColorSpace getFillColorSpace() { return fillColorSpace; } public void setFillColorSpace(PColorSpace fillColorSpace) { this.fillColorSpace = fillColorSpace; } public PColorSpace getStrokeColorSpace() { return strokeColorSpace; } public void setStrokeColorSpace(PColorSpace strokeColorSpace) { this.strokeColorSpace = strokeColorSpace; } public TextState getTextState() { return textState; } public void setTextState(TextState textState) { this.textState = textState; } public int getOverprintMode() { return overprintMode; } public boolean isOverprintStroking() { return overprintStroking; } public boolean isOverprintOther() { return overprintOther; } public void setOverprintMode(int overprintMode) { this.overprintMode = overprintMode; } public void setOverprintStroking(boolean overprintStroking) { this.overprintStroking = overprintStroking; } public void setOverprintOther(boolean overprintOther) { this.overprintOther = overprintOther; } public int getAlphaRule() { return alphaRule; } public void setAlphaRule(int alphaRule) { this.alphaRule = alphaRule; } public boolean isTransparencyGroup() { return transparencyGroup; } public void setTransparencyGroup(boolean transparencyGroup) { this.transparencyGroup = transparencyGroup; } public boolean isIsolated() { return isolated; } public void setIsolated(boolean isolated) { this.isolated = isolated; } public boolean isKnockOut() { return knockOut; } public void setKnockOut(boolean knockOut) { this.knockOut = knockOut; } public ExtGState getExtGState() { return extGState; } }