/* * $Id: PDFGraphics.java,v 1.6 2007/09/22 12:58:40 gil1 Exp $ * * $Date: 2007/09/22 12:58:40 $ * * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package gnu.jpdf; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.Image; import java.awt.Paint; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.RenderingHints.Key; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ColorModel; import java.awt.image.ImageObserver; import java.awt.image.RenderedImage; import java.awt.image.WritableRaster; import java.awt.image.renderable.RenderableImage; import java.awt.print.PageFormat; import java.io.PrintWriter; import java.io.Serializable; import java.text.DecimalFormat; import java.util.Hashtable; import java.util.Map; import java.util.WeakHashMap; /** * This class is our implementation of AWT's Graphics class. It provides a * Java standard way of rendering into a PDF Document's Page. * * @author Peter T Mount, http://www.retep.org.uk/pdf/ * @author Eric Z. Beard, ericzbeard@hotmail.com * @author Gilbert DeLeeuw, gil1@users.sourceforge.net * @version $Revision: 1.6 $, $Date: 2007/09/22 12:58:40 $ * @see gnu.jpdf.PDFGraphics */ public class PDFGraphics extends Graphics2D implements Serializable { /** * One degree in radians */ private static final double degrees_to_radians = Math.PI/180.0; private static final int FILL = 1; private static final int STROKE = 2; private static final int CLIP = 3; private static final AffineTransform IDENTITY = new AffineTransform(); private static final Stroke DEF_STROKE = new BasicStroke(); /* * NOTE: The original class is the work of Peter T. Mount, who released it * in the uk.org.retep.pdf package. It was modified by Eric Z. Beard as * follows: * The package name was changed to gnu.pdf. * The formatting was changed a little bit. * This used to subclass an abstract class in a different package with * the same name (confusing). Now it's one concrete class. * drawImage() was implemented * It is still licensed under the LGPL. */ // Implementation notes: // // Pages 333-335 of the PDF Reference Manual // // Unless absolutely required, use the moveto, lineto and rectangle // operators to perform those actions. // They contain some extra optimizations // which will reduce the output size by up to half in some cases. // // About fill operators: For correct operation, any fill operation should // start with closeBlock(), which will ensure any previous path is completed, // otherwise you may find the fill will include previous items private static final DecimalFormat df = new DecimalFormat("#.###"); //JPEXS: cache for already used images private static Map<Image,PDFImage> usedImages = new WeakHashMap<Image, PDFImage>(); private Color background; /** * This is true for any Graphics instance that didn't create the stream. * @see #create */ private boolean child; private Area clip; /** * This holds the current clipRectangle */ protected Rectangle clipRectangle; private Composite composite; private Graphics2D dg2 = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB).createGraphics(); /** * This is the current font (in Java format) */ private Font font; /** * Part of the optimizer: * When true, we are drawing a path. */ private boolean inStroke; /** * Part of the optimizer: * When true, we are within a Text Block. */ private boolean inText; // true if within a Text Block - see newTextBlock() /** * The stroke line cap code; */ private int lineCap = 0; /** * The stroke line join code */ private int lineJoin = 0; /** * The stroke line width */ private float lineWidth = 1.0f; /** * Part of the optimizer: * The last known moveto/lineto x coordinate * @see #moveto * @see #lineto */ private float lx; // last known moveto/lineto coordinates /** * Part of the optimizer: * The last known moveto/lineto y coordinate * @see #moveto * @see #lineto */ private float ly; // last known moveto/lineto coordinates private float miterLimit = 10.0f; /** * Part of the optimizer: * When true, the font has changed. */ private boolean newFont; // true if the font changes - see newTextBlock() private Stroke originalStroke; // Original transform private AffineTransform oTransform; /** * This is a reference to the PDFPage we are rendering to. */ private PDFPage page; /** * This is the current pen/fill color */ private Paint paint; /** * This is the current font (in PDF format) */ private PDFFont pdffont; /** * Part of the optimizer: * This is written to the stream when the newPath() is called. np then clears * this value. */ private String pre_np; // PDF space transform private AffineTransform pTransform; /** * This is the PrintWriter used to write PDF drawing commands to the Stream */ private PrintWriter pw; /** * RenderingHints */ private RenderingHints rhints = new RenderingHints(null); private Stroke stroke; // Start of Graphics2D properties // Java space transform private AffineTransform transform; /** * This is used to translate coordinates */ protected float trax; /** * This is used to translate coordinates */ protected float tray; /** * Part of the optimizer: * The last x coordinate when rendering text */ private float tx; // the last coordinate for text rendering /** * Part of the optimizer: * The last y coordinate when rendering text */ private float ty; // the last coordinate for text rendering /** * @see Graphics2D#addRenderingHints(Map) */ public void addRenderingHints(Map<?,?> hints) { rhints.putAll(hints); } /** * This produces an arc by breaking it down into one or more Bezier curves. * It is used internally to implement the drawArc and fillArc methods. * * @param axc X coordinate of arc centre * @param ayc Y coordinate of arc centre * @param width of bounding rectangle * @param height of bounding rectangle * @param ang1 Start angle * @param ang2 End angle * @param clockwise true to draw clockwise, false anti-clockwise */ public void arc(double axc,double ayc, double width,double height, double ang1,double ang2, boolean clockwise) { double adiff; double x0, y0; double x3r, y3r; boolean first = true; // may not need this //if( ar < 0 ) { //ang1 += fixed_180; //ang2 += fixed_180; //ar = - ar; //} double ang1r = (ang1%360.0)*degrees_to_radians; double sin0 = Math.sin(ang1r); double cos0 = Math.cos(ang1r); x0 = axc + width*cos0; y0 = ayc + height*sin0; // NB: !clockwise here as Java Space is inverted to User Space if( !clockwise ) { // Quadrant reduction while ( ang1 < ang2 ) ang2 -= 360.0; while ( (adiff = ang2 - ang1) < -90.0 ) { double w = sin0; sin0 = -cos0; cos0 = w; x3r = axc + width*cos0; y3r = ayc + height*sin0; arc_add(first, width, height, x0, y0, x3r, y3r, (x0 + width*cos0), (y0 + height*sin0) ); x0 = x3r; y0 = y3r; ang1 -= 90.0; first = false; } } else { // Quadrant reduction while ( ang2 < ang1 ) ang2 += 360.0; while ( (adiff = ang2 - ang1) > 90.0 ) { double w = cos0; cos0 = -sin0; sin0 = w; x3r = axc + width*cos0; y3r = ayc + height*sin0; arc_add(first, width, height, x0, y0, x3r, y3r, (x0 + width*cos0), (y0 + height*sin0) ); x0 = x3r; y0 = y3r; ang1 += 90.0; first = false; } } // Compute the intersection of the tangents. // We know that -fixed_90 <= adiff <= fixed_90. double trad = Math.tan(adiff * (degrees_to_radians / 2)); double ang2r = ang2 * degrees_to_radians; double xt = x0 - trad * width*sin0; double yt = y0 + trad * height*cos0; arc_add(first, width, height, x0, y0, (axc + width * Math.cos(ang2r)), (ayc + height * Math.sin(ang2r)), xt, yt); } /** * Used by the arc method to actually add an arc to the path * Important: We write directly to the stream here, because this method * operates in User space, rather than Java space. * @param first true if the first arc * @param w width * @param h height * @param x0 coordinate * @param y0 coordinate * @param x3 coordinate * @param y3 coordinate * @param xt coordinate * @param yt coordinate */ private void arc_add(boolean first, double w,double h, double x0,double y0, double x3,double y3, double xt,double yt) { double dx = xt - x0, dy = yt - y0; double dist = dx*dx + dy*dy; double w2 = w*w, h2=h*h; double r2 = w2+h2; double fw = 0.0, fh = 0.0; if(dist < (r2*1.0e8)) { // JM fw = (w2 != 0.0) ? ((4.0/3.0)/(1+Math.sqrt(1+dist/w2))) : 0.0; fh = (h2 != 0.0) ? ((4.0/3.0)/(1+Math.sqrt(1+dist/h2))) : 0.0; } // The path must have a starting point if(first) moveto(x0,y0); double x = x0+((xt-x0)*fw); double y = y0+((yt-y0)*fh); x0 = x3+((xt-x3)*fw); y0 = y3+((yt-y3)*fh); // Finally the actual curve. curveto(x,y,x0,y0,x3,y3); } /** * This simply draws a White Rectangle to clear the area * @param x coordinate * @param y coordinate * @param w width * @param h height */ public void clearRect(int x,int y,int w,int h) { closeBlock(); pw.print("q 1 1 1 RG ");// save state, set colour to White drawRect(x,y,w,h); closeBlock("B Q"); // close fill & stroke, then restore state } /** * @see Graphics2D#clip(Shape) */ public void clip(Shape s) { if (s == null) { setClip(null); return; } s = transform.createTransformedShape(s); if (clip == null) clip = new Area(s); else clip.intersect(new Area(s)); // followPath(s, CLIP); } /** * This extra method allows PDF users to clip to a Polygon. * * <p>In theory you could use setClip(), except that java.awt.Graphics * only supports Rectangle with that method, so we will have an extra * method. * @param p Polygon to clip to */ public void clipPolygon(Polygon p) { closeBlock(); // finish off any existing path polygon(p.xpoints,p.ypoints,p.npoints); closeBlock("W"); // clip to current path clipRectangle = p.getBounds(); } /** * Clips to a set of coordinates * @param x coordinate * @param y coordinate * @param w width * @param h height */ public void clipRect(int x,int y,int w,int h) { setClip(x,y,w,h); } /** * All functions should call this to close any existing optimized blocks. */ void closeBlock() { closeBlock("S"); } /** * <p>This is used by code that use the path in any way other than Stroke * (like Fill, close path & Stroke etc). Usually this is used internally.</p> * * @param code PDF operators that will close the path */ void closeBlock(String code) { if(inText) { pw.println("ET Q"); // setOrientation(); // fixes Orientation matrix } if(inStroke) { pw.println(code); } inStroke=inText=false; } /** * This is unsupported - how do you do this with Vector graphics? * @param x coordinate * @param y coordinate * @param w width * @param h height * @param dx coordinate * @param dy coordinate */ public void copyArea(int x,int y,int w,int h,int dx,int dy) { // Hmm... Probably need to keep track of everything // that has been drawn so far to get the contents of an area } //============ Line operations ======================= /** * <p>This returns a child instance of this Graphics object. As with AWT, the * affects of using the parent instance while the child exists, is not * determined.</p> * * <p>Once complete, the child should be released with it's dispose() * method which will restore the graphics state to it's parent.</p> * * @return Graphics object to render onto the page */ public Graphics create() { closeBlock(); PDFGraphics g = createGraphic(page,pw); // The new instance inherits a few items // g.media = new Rectangle(media); g.trax = trax; g.tray = tray; g.clipRectangle = new Rectangle(clipRectangle); return (Graphics) g; } // end create() /** * This method creates a new instance of the class based on the page * and a print writer. * * @param page the page to attach to * @param pw the <code>PrintWriter</code> to attach to. */ protected PDFGraphics createGraphic(PDFPage page, PrintWriter pw) { PDFGraphics g = new PDFGraphics(); g.init(page,pw); return g; } /** * This extension appends a Bezier curve to the path. The curve * extends from the current point to (x2,y2) using the current * point and (x1,y1) as the Bezier control points. * <p>The new current point is (x2,y2) * * @param x1 Second control point * @param y1 Second control point * @param x2 Destination point * @param y2 Destination point */ public void curveto(double x1,double y1,double x2,double y2) { newPath(); pw.println(cxy(x1,y1)+cxy(x2,y2)+"v"); lx=(float)x2; ly=(float)y2; } /** * This extension appends a Bezier curve to the path. The curve * extends from the current point to (x3,y3) using (x1,y1) and * (x2,y2) as the Bezier control points. * <p>The new current point is (x3,y3) * * @param x1 First control point * @param y1 First control point * @param x2 Second control point * @param y2 Second control point * @param x3 Destination point * @param y3 Destination point */ public void curveto(double x1,double y1,double x2,double y2,double x3,double y3) { newPath(); pw.println(cxy(x1,y1)+cxy(x2,y2)+cxy(x3,y3)+"c"); lx=(float)x3; ly=(float)y3; } /** * This extension appends a Bezier curve to the path. The curve * extends from the current point to (x2,y2) using the current * point and (x1,y1) as the Bezier control points. * <p>The new current point is (x2,y2) * * @param x1 Second control point * @param y1 Second control point * @param x2 Destination point * @param y2 Destination point */ public void curveto(int x1,int y1,int x2,int y2) { newPath(); pw.println(cxy(x1,y1)+cxy(x2,y2)+"v"); lx=x2; ly=y2; } /** * This extension appends a Bezier curve to the path. The curve * extends from the current point to (x3,y3) using (x1,y1) and * (x2,y2) as the Bezier control points. * <p>The new current point is (x3,y3) * * @param x1 First control point * @param y1 First control point * @param x2 Second control point * @param y2 Second control point * @param x3 Destination point * @param y3 Destination point */ public void curveto(int x1,int y1,int x2,int y2,int x3,int y3) { newPath(); pw.println(cxy(x1,y1)+cxy(x2,y2)+cxy(x3,y3)+"c"); lx=x3; ly=y3; } /** * This extension appends a Bezier curve to the path. The curve * extends from the current point to (x2,y2) using (x1,y1) and * the end point as the Bezier control points. * <p>The new current point is (x2,y2) * * @param x1 Second control point * @param y1 Second control point * @param x2 Destination point * @param y2 Destination point */ public void curveto2(double x1,double y1,double x2,double y2) { newPath(); pw.println(cxy(x1,y1)+cxy(x2,y2)+"y"); lx=(float)x2; ly=(float)y2; } // Arcs are horrible and complex. They are at the end of the // file, because they are the largest. This is because, unlike // Postscript, PDF doesn't have any arc operators, so we must // implement them by converting into one or more Bezier curves // (which is how Postscript does them internally). /** * This extension appends a Bezier curve to the path. The curve * extends from the current point to (x2,y2) using (x1,y1) and * the end point as the Bezier control points. * <p>The new current point is (x2,y2) * * @param x1 Second control point * @param y1 Second control point * @param x2 Destination point * @param y2 Destination point */ public void curveto2(int x1,int y1,int x2,int y2) { newPath(); pw.println(cxy(x1,y1)+cxy(x2,y2)+"y"); lx=x2; ly=y2; } /** * Converts the Java space dimension into pdf. * @param w width * @param h height * @return String containing the coordinates in PDF space */ private String cwh(double w,double h) { double nw=w,nh=h; // scratch // switch(mediaRot) { // case PageFormat.PORTRAIT: // Portrait //nw = w; nh = -h; // break; // // case PageFormat.LANDSCAPE: // // Landscape // nw = h; // nh = w; // break; // //// case 180: //// // Inverse Portrait //// nw = -w; //// //nh = h; //// break; // // case PageFormat.REVERSE_LANDSCAPE: // // Seascape // nw = -h; // nh = -w; // break; // } return ""+df.format(nw)+" "+df.format(nh)+" "; } /** * Converts the Java space dimension into pdf. * @param w width * @param h height * @return String containing the coordinates in PDF space */ private String cwh(int w,int h) { return cwh((double)w,(double)h); } /** * Converts the Java space coordinates into pdf. * @param x coordinate * @param y coordinate * @return String containing the coordinates in PDF space */ private String cxy(double x, double y) { // double nx = x, ny = y; // scratch // double mh = page.getPageFormat().getHeight(); Point2D ptSrc = new Point2D.Double(x, y); Point2D ptDst = new Point2D.Double(); transform.transform(ptSrc, ptDst); // x += trax; // y += tray; // // nx = x; // ny = mh - y; // // System.out.println("\ncxy(" + ptSrc.getX() + ", " + ptSrc.getY() + ")"); // System.out.println("Old [" + nx + "," + ny + "]"); // System.out.println("Trn [" + ptDst.getX() + ", " + ptDst.getY() + "]"); // // return "" + df.format(nx) + " " + df.format(ny) + " "; return ""+df.format(ptDst.getX())+" "+df.format(ptDst.getY())+" "; } /** * Converts the Java space coordinates into pdf. * @param x coordinate * @param y coordinate * @return String containing the coordinates in PDF space */ private String cxy(int x,int y) { return cxy((double)x,(double)y); } /** * <p>This releases any resources used by this Graphics object. You must use * this method once finished with it. Leaving it open will leave the PDF * stream in an inconsistent state, and will produce errors.</p> * * <p>If this was created with Graphics.create() then the parent instance * can be used again. If not, then this closes the graphics operations for * this page when used with PDFJob.</p> * * <p>When using PDFPage, you can create another fresh Graphics instance, * which will draw over this one.</p> * */ public void dispose() { closeBlock(); if(child) { pw.println("Q"); // restore graphics context } else { pw.close(); // close the stream if were the parent } } // ********************************************* // **** Implementation of java.awt.Graphics **** // ********************************************* //============ Rectangle operations ======================= /** * @see Graphics2D#draw(Shape) */ public void draw(Shape s) { followPath(s, STROKE); } /** * <p>Not implemented</p> * * <p>Draws a 3-D highlighted outline of the specified rectangle. * The edges of the rectangle are highlighted so that they appear * to be beveled and lit from the upper left corner. * The colors used for the highlighting effect are determined based on * the current color. The resulting rectangle covers an area that * is width + 1 pixels wide by height + 1 pixels tall. *</p> * * @param x an <code>int</code> value * @param y an <code>int</code> value * @param width an <code>int</code> value * @param height an <code>int</code> value * @param raised a <code>boolean</code> value */ public void draw3DRect(int x, int y, int width, int height, boolean raised) { // Not implemented } /** * Draws an arc * @param x coordinate * @param y coordinate * @param w width * @param h height * @param sa Start angle * @param aa End angle */ public void drawArc(int x,int y,int w,int h,int sa,int aa) { w=w>>1; h=h>>1; x+=w; y+=h; arc((double)x,(double)y, (double)w,(double)h, (double)-sa,(double)(-sa-aa), false); } /** * <p>Not implemented</p> * * @param data a <code>byte[]</code> value * @param offset an <code>int</code> value * @param length an <code>int</code> value * @param x an <code>int</code> value * @param y an <code>int</code> value */ public void drawBytes(byte[] data, int offset, int length, int x, int y) { } //============ Optimizers ======================= /** * @see Graphics2D#drawGlyphVector(GlyphVector, float, float) */ public void drawGlyphVector(GlyphVector g, float x, float y) { Shape s = g.getOutline(x, y); fill(s); } /** * @see Graphics2D#drawImage(BufferedImage, BufferedImageOp, int, int) */ public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { BufferedImage result = img; if (op != null) { result = op.createCompatibleDestImage(img, img.getColorModel()); result = op.filter(img, result); } drawImage(result, x, y, null); } /** * @see Graphics2D#drawImage(Image, AffineTransform, ImageObserver) */ public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { // return drawImage(img, null, xform, null, obs); return true; } /** * <p>Draw's an image onto the page, with a backing colour.</p> * * @param img The java.awt.Image * @param x coordinate on page * @param y coordinate on page * @param bgcolor Background colour * @param obs ImageObserver * @return true if drawn */ public boolean drawImage(Image img,int x,int y,Color bgcolor, ImageObserver obs) { return drawImage(img,x,y,img.getWidth(obs),img.getHeight(obs), bgcolor,obs); } /** * Draw's an image onto the page * @param img The java.awt.Image * @param x coordinate on page * @param y coordinate on page * @param obs ImageObserver * @return true if drawn */ public boolean drawImage(Image img,int x,int y,ImageObserver obs) { return drawImage(img,x,y,img.getWidth(obs),img.getHeight(obs),obs); } /** * <p>Draw's an image onto the page, with a backing colour.</p> * * @param img The java.awt.Image * @param x coordinate on page * @param y coordinate on page * @param w Width on page * @param h height on page * @param bgcolor Background colour * @param obs ImageObserver * @return true if drawn */ public boolean drawImage(Image img,int x,int y,int w,int h, Color bgcolor,ImageObserver obs) { closeBlock(); pw.print("q "); // save state Color c = getColor(); // save current colour setColor(bgcolor); // change the colour drawRect(x,y,w,h); closeBlock("B Q"); // fill stroke, restore state paint = c; // restore original colour return drawImage(img,x,y,img.getWidth(obs),img.getHeight(obs),obs); } /** * <p>Draws an image onto the page.</p> * * <p>This method is implemented with ASCIIbase85 encoding and the * zip stream deflater. It results in a stream that is anywhere * from 3 to 10 times as big as the image. This obviously needs some * improvement, but it works well for small images</p> * * @param img The java.awt.Image * @param x coordinate on page * @param y coordinate on page * @param w Width on page * @param h height on page * @param obs ImageObserver * @return true if drawn */ public boolean drawImage(Image img,int x,int y,int w,int h, ImageObserver obs) { closeBlock(); PDFImage image; if(usedImages.containsKey(img)){ image = usedImages.get(img); }else{ image = new PDFImage(img,obs); // The image needs to be registered in several places page.getPDFDocument().setImageName(image); page.getPDFDocument().add(image); usedImages.put(img, image); } page.addToProcset("/ImageC"); page.addImageResource(image.getName() + " " + image.getSerialID() + " 0 R"); // JM /*page.addResource("/XObject << " + image.getName() + " " + image.getSerialID() + " 0 R >>");*/ // q w 0 0 h x y cm % the coordinate matrix pw.print("q " + w+ " 0 0 " + h+ " " + x + " " + ((int)page.getDimension().getHeight()-y-h) + " cm \n" + image.getName() + " Do\nQ\n"); return false; } /** * Draw's an image onto the page, with scaling * <p>This is not yet supported. * * @param img The java.awt.Image * @param dx1 coordinate on page * @param dy1 coordinate on page * @param dx2 coordinate on page * @param dy2 coordinate on page * @param sx1 coordinate on image * @param sy1 coordinate on image * @param sx2 coordinate on image * @param sy2 coordinate on image * @param bgcolor Background colour * @param obs ImageObserver * @return true if drawn */ public boolean drawImage(Image img,int dx1,int dy1,int dx2, int dy2,int sx1,int sy1,int sx2,int sy2, Color bgcolor,ImageObserver obs) { return false; } //============ Clipping operations ======================= /** * Draw's an image onto the page, with scaling * <p>This is not yet supported. * * @param img The java.awt.Image * @param dx1 coordinate on page * @param dy1 coordinate on page * @param dx2 coordinate on page * @param dy2 coordinate on page * @param sx1 coordinate on image * @param sy1 coordinate on image * @param sx2 coordinate on image * @param sy2 coordinate on image * @param obs ImageObserver * @return true if drawn */ public boolean drawImage(Image img,int dx1,int dy1,int dx2, int dy2,int sx1,int sy1,int sx2,int sy2, ImageObserver obs) { // This shouldn't be too bad, just change the coordinate matrix return false; } /** * Draws a line between two coordinates. * * If the first coordinate is the same as the last one drawn * (i.e. a previous drawLine, moveto, etc) it is ignored. * @param x1 coordinate * @param y1 coordinate * @param x2 coordinate * @param y2 coordinate */ public void drawLine(int x1,int y1,int x2,int y2) { moveto(x1,y1); lineto(x2,y2); } //============ Arcs operations ============================== // These are the standard Graphics operators. They use the // arc extension operators to achieve the affect. /** * <p>Draws an oval</p> * * @param x coordinate * @param y coordinate * @param w width * @param h height */ public void drawOval(int x,int y,int w,int h) { drawArc(x, y, w, h, 0, 360); } /** * Draws a polygon, linking the first and last coordinates. * @param xp Array of x coordinates * @param yp Array of y coordinates * @param np number of points in polygon */ public void drawPolygon(int[] xp,int[] yp,int np) { polygon(xp,yp,np); closeBlock("s"); // close path and stroke } /** * Draws a polyline. The first and last coordinates are not linked. * @param xp Array of x coordinates * @param yp Array of y coordinates * @param np number of points in polyline */ public void drawPolyline(int[] xp,int[] yp,int np) { polygon(xp,yp,np); // no stroke, as we keep the optimizer in stroke state } /** * We override Graphics.drawRect as it doesn't join the 4 lines. * Also, PDF provides us with a Rectangle operator, so we will use that. * @param x coordinate * @param y coordinate * @param w width * @param h height */ public void drawRect(int x,int y,int w,int h) { newPath(); pw.print(cxy(x,y)+cwh(w,h)+"re "); // rectangle lx=x; // I don't know if this is correct, but lets see if PDF ends ly=y; // the rectangle at it's start. // stroke (optimized) } /** * @see Graphics2D#drawRenderableImage(RenderableImage, AffineTransform) */ public void drawRenderableImage(RenderableImage img, AffineTransform xform) { drawRenderedImage(img.createDefaultRendering(), xform); } /** * @see Graphics2D#drawRenderedImage(RenderedImage, AffineTransform) */ public void drawRenderedImage(RenderedImage img, AffineTransform xform) { BufferedImage image = null; if (img instanceof BufferedImage) { image = (BufferedImage)img; } else { ColorModel cm = img.getColorModel(); int width = img.getWidth(); int height = img.getHeight(); WritableRaster raster = cm.createCompatibleWritableRaster(width, height); boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); Hashtable<String, Object> properties = new Hashtable<String, Object>(); String[] keys = img.getPropertyNames(); if (keys!=null) { for (int i = 0; i < keys.length; i++) { properties.put(keys[i], img.getProperty(keys[i])); } } BufferedImage result = new BufferedImage(cm, raster, isAlphaPremultiplied, properties); img.copyData(raster); image=result; } drawImage(image, xform, null); } /** * This is not yet implemented * * @param x coordinate * @param y coordinate * @param w width * @param h height * @param aw a-width * @param ah a-height */ public void drawRoundRect(int x,int y,int w,int h,int aw,int ah) { } //============ Oval operations ======================= /** * Draws a string using a AttributedCharacterIterator. * <p>This is not supported yet, as I have no idea what an * AttributedCharacterIterator is. * <p>This method is new to the Java2 API. */ public void drawString(java.text.AttributedCharacterIterator aci, float x,float y) { } /** * Draws a string using a AttributedCharacterIterator. * <p>This is not supported yet, as I have no idea what an * AttributedCharacterIterator is. * <p>This method is new to the Java2 API. */ public void drawString(java.text.AttributedCharacterIterator aci, int x,int y) { } public void drawString(String s,float x,float y) { newTextBlock(x, y); pw.println(PDFStringHelper.makePDFString(s)+" Tj"); } /** * This draws a string. * * @oaran s String to draw * @param x coordinate * @param y coordinate */ public void drawString(String s,int x,int y) { newTextBlock(x,y); pw.println(PDFStringHelper.makePDFString(s)+" Tj"); } /** * @see Graphics2D#fill(Shape) */ public void fill(Shape s) { followPath(s, FILL); } /** * <p>Not implemented</p> * * @param x an <code>int</code> value * @param y an <code>int</code> value * @param width an <code>int</code> value * @param height an <code>int</code> value * @param raised a <code>boolean</code> value */ public void fill3DRect(int x, int y, int width, int height, boolean raised) { // Not implemented } /** * Fills an arc, joining the start and end coordinates * @param x coordinate * @param y coordinate * @param w width * @param h height * @param sa Start angle * @param aa End angle */ public void fillArc(int x,int y,int w,int h,int sa,int aa) { // here we fool the optimizer. We force any open path to be closed, // then draw the arc. Finally, as the optimizer hasn't stroke'd the // path, we close and fill it, and mark the Stroke as closed. // // Note: The lineto to the centre of the object is required, because // the fill only fills the arc. Skipping this includes an extra // chord, which isn't correct. Peter May 31 2000 closeBlock(); drawArc(x,y,w,h,sa,aa); lineto(x+(w>>1),y+(h>>1)); closeBlock("b"); // closepath and fill } //============ Extension operations ============================== // These are extensions, and provide access to PDF Specific // operators. /** * <p>Draws a filled oval</p> * * @param x coordinate * @param y coordinate * @param w width * @param h height */ public void fillOval(int x,int y,int w,int h) { fillArc(x, y, w, h, 0, 360); } //============ Polygon operations ======================= /** * Fills a polygon. * @param xp Array of x coordinates * @param yp Array of y coordinates * @param np number of points in polygon */ public void fillPolygon(int[] xp,int[] yp,int np) { closeBlock(); // finish off any previous paths polygon(xp,yp,np); closeBlock("b"); // closepath, fill and stroke } //============ Image operations ======================= /** * Fills a rectangle with the current colour * * @param x coordinate * @param y coordinate * @param w width * @param h height */ public void fillRect(int x,int y,int w,int h) { // end any path & stroke. This ensures the fill is on this // rectangle, and not on any previous graphics closeBlock(); drawRect(x,y,w,h); closeBlock("B"); // rectangle, fill stroke } //============ Round Rectangle operations ======================= /** * This is not yet implemented * * @param x coordinate * @param y coordinate * @param w width * @param h height * @param aw a-width * @param ah a-height */ public void fillRoundRect(int x,int y,int w,int h,int aw,int ah) { } /////////////////////////////////////////////// // // // implementation specific methods // // private void followPath(Shape s, int drawType) { PathIterator points; if (s==null) return; if (drawType==STROKE) { if (!(stroke instanceof BasicStroke)) { s = stroke.createStrokedShape(s); followPath(s, FILL); return; } } // if (drawType==STROKE) { // setStrokeDiff(stroke, oldStroke); // oldStroke = stroke; // setStrokePaint(); // } // else if (drawType==FILL) // setFillPaint(); points = s.getPathIterator(IDENTITY); int segments = 0; float[] coords = new float[6]; while(!points.isDone()) { segments++; int segtype = points.currentSegment(coords); switch(segtype) { case PathIterator.SEG_CLOSE: pw.print("h "); break; case PathIterator.SEG_CUBICTO: curveto(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); break; case PathIterator.SEG_LINETO: lineto(coords[0], coords[1]); break; case PathIterator.SEG_MOVETO: moveto(coords[0], coords[1]); break; case PathIterator.SEG_QUADTO: curveto(coords[0], coords[1], coords[2], coords[3]); break; } points.next(); } switch (drawType) { case FILL: if (segments > 0) { if (points.getWindingRule() == PathIterator.WIND_EVEN_ODD) closeBlock("B*"); else closeBlock("B"); } break; case STROKE: if (segments > 0) closeBlock("S"); break; case CLIP: default: //drawType==CLIP if (segments == 0) drawRect(0, 0, 0, 0); if (points.getWindingRule() == PathIterator.WIND_EVEN_ODD) closeBlock("W*"); else closeBlock("W"); } } /** * @see Graphics2D#getBackground() */ public Color getBackground() { return background; } /** * Returns the Shape of the clipping region * As my JDK docs say, this may break with Java 2D. * @return Shape of the clipping region */ public Shape getClip() { return null; } /** * Returns the Rectangle that fits the current clipping region * @return the Rectangle that fits the current clipping region */ public Rectangle getClipBounds() { return clipRectangle; } //============ Color operations ======================= /** * Returns the current pen Colour * @return the current pen Colour */ public Color getColor() { return (paint instanceof Color) ? (Color) paint : Color.black; } /** * @see Graphics2D#getComposite() */ public Composite getComposite() { return composite; } /** * @see Graphics2D#getDeviceConfiguration() */ public GraphicsConfiguration getDeviceConfiguration() { return dg2.getDeviceConfiguration(); } /** * Return's the current font. * @return the current font. */ public Font getFont() { if(font==null) setFont(new Font("SansSerif",Font.PLAIN,12)); return font; } /** * Returns the FontMetrics for a font. * <p>This doesn't work correctly. Perhaps having some way of mapping * the base 14 fonts to our own FontMetrics implementation? * @param font The java.awt.Font to return the metrics for * @return FontMetrics for a font */ public FontMetrics getFontMetrics(Font font) { Frame dummy = new Frame(); dummy.addNotify(); Image image = dummy.createImage(100, 100); if (image == null) { System.err.println("getFontMetrics: image is null"); } Graphics graphics = image.getGraphics(); return graphics.getFontMetrics(font); } /** * @see Graphics2D#getFontRenderContext() */ public FontRenderContext getFontRenderContext() { boolean antialias = RenderingHints.VALUE_TEXT_ANTIALIAS_ON.equals(getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING)); boolean fractions = RenderingHints.VALUE_FRACTIONALMETRICS_ON.equals(getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS)); return new FontRenderContext(new AffineTransform(), antialias, fractions); } /** * Returns the associated PDFPage for this graphic * @return the associated PDFPage for this graphic */ public PDFPage getPage() { return page; } /** * Returns the current pen Colour * @return the current pen Colour */ public Paint getPaint() { return paint; } /** * @param arg0 a key * @return the rendering hint */ public Object getRenderingHint(Key arg0) { return rhints.get(arg0); } /** * @see Graphics2D#getRenderingHints() */ public RenderingHints getRenderingHints() { return rhints; } /** * @see Graphics2D#getStroke() */ public Stroke getStroke() { return originalStroke; } /** * @see Graphics2D#getTransform() */ public AffineTransform getTransform() { return new AffineTransform(oTransform); } /** * Returns the PrintWriter handling the underlying stream * @return the PrintWriter handling the underlying stream */ public PrintWriter getWriter() { return pw; } /** * @see Graphics2D#hit(Rectangle, Shape, boolean) */ public boolean hit(Rectangle rect, Shape s, boolean onStroke) { if (onStroke) { s = stroke.createStrokedShape(s); } s = transform.createTransformedShape(s); Area area = new Area(s); if (clip != null) area.intersect(clip); return area.intersects(rect.x, rect.y, rect.width, rect.height); } /** * This initialises the stream by saving the current graphics state, and * setting up the default line width (for us). * * It also sets up the instance ready for graphic operations and any * optimisations. * * <p>For child instances, the stream is already open, so this should keep * things happy. */ private void init() { PageFormat pf = page.getPageFormat(); // save graphics state (restored by dispose) if(child) { pw.print("q "); } // now initialise the instance //setColor(Color.black); paint = Color.black; // possible: if parent.color is not black, then force black? // must check to see what AWT does? // Original User Space Transform (identity) oTransform = new AffineTransform(); // Transform from Java Space to PDF Space pTransform = new AffineTransform(); pTransform.translate(0, pf.getHeight()); pTransform.scale(1d, -1d); // Combined Transform User->Java->PDF transform = new AffineTransform(oTransform); transform.concatenate(pTransform); // Set the line width setStroke(DEF_STROKE); } /** * This is called by PDFPage when creating a Graphcis instance. * @param page The PDFPage to draw onto. */ protected void init(PDFPage page) { this.page = page; // We are the parent instance child = false; // Now create a stream to store the graphics in PDFStream stream = new PDFStream(); // To view detail in uncompressed format comment out the next line stream.setDeflate(true); page.getPDFDocument().add(stream); page.add(stream); pw = stream.getWriter(); // initially, we are limited to the page size clipRectangle = page.getImageableArea(); // finally initialise the stream init(); } /** * This method is used internally by create() and by the PDFJob class * @param page PDFPage to draw into * @param pw PrintWriter to use */ protected void init(PDFPage page,PrintWriter pw) { this.page = page; this.pw = pw; // In this case, we didn't create the stream (our parent did) // so child is true (see dispose) child = true; // finally initialise the stream init(); } /** * This adds a line segment to the current path * @param x coordinate * @param y coordinate */ public void lineto(double x,double y) { newPath(); // no optimisation here as it may introduce errors on decimal coordinates. pw.print(cxy(x,y)+"l "); lx=(float)x; ly=(float)y; } /** * This adds a line segment to the current path * @param x coordinate * @param y coordinate */ public void lineto(int x,int y) { newPath(); if(lx!=x && ly!=y) pw.print(cxy(x,y)+"l "); lx=x; ly=y; } /** * This moves the current drawing point. * @param x coordinate * @param y coordinate */ public void moveto(double x,double y) { newPath(); // no optimisation here as it may introduce errors on decimal coordinates. pw.print(cxy(x,y)+"m "); lx=(float)x; ly=(float)y; } /** * This moves the current drawing point. * @param x coordinate * @param y coordinate */ public void moveto(int x,int y) { newPath(); if(lx!=x || ly!=y) pw.print(cxy(x,y)+"m "); lx=x; ly=y; } /** * Functions that draw lines should start by calling this. It starts a * new path unless inStroke is set, in that case it uses the existing path */ void newPath() { if(inText) { closeBlock(); } if(!inStroke) { if(pre_np!=null) { pw.print(pre_np); // this is the prefix set by setOrientation() pre_np = null; } pw.print("n "); } inText=false; inStroke=true; // an unlikely coordinate to fool the moveto() optimizer lx = ly = -9999; } /** * <p>Functions that draw text should start by calling this. It starts a text * block (accounting for media orientation) unless we are already in a Text * block.</p> * * <p>It also handles if the font has been changed since the current text * block was started, so your function will be current.</p> * * @param x x coordinate in java space * @param y y coordinate in java space */ void newTextBlock(float x,float y) { // close the current path if there is one if(inStroke) { closeBlock(); } // create the text block if one is not current. If we are, the newFont // condition at the end catches font changes if(!inText) { // This ensures that there is a font available getFont(); pw.print("q BT "); tx=ty=0; // produce the text matrix for the media // switch(mediaRot) { // case PageFormat.PORTRAIT: // Portrait // //pw.println("1 0 0 1 0 0 Tm"); // break; // // case PageFormat.LANDSCAPE: // Landscape // pw.println("0 1 -1 0 0 0 Tm"); // rotate // break; // // case 180: // Inverted Portrait // pw.println("1 0 0 -1 0 0 Tm"); // break; // // case PageFormat.REVERSE_LANDSCAPE: // Seascape // pw.println("0 -1 1 0 0 0 Tm"); // rotate // break; // } // move the text cursor by an absolute amount pw.print(txy(x,y)+"Td "); } else { // move the text cursor by a relative amount //int ox=x-tx, oy=ty-y; //pw.print(""+ox+" "+oy+" Td "); //pw.print(cwh(x-tx,y-ty)+"Td "); pw.print(twh(x,y,tx,ty)+"Td "); } // preserve the coordinates for the next time tx = x; ty = y; if(newFont || !inText) pw.print(pdffont.getName()+" "+font.getSize()+" Tf "); // later add colour changes here (if required) inStroke = newFont = false; inText = true; } /** * This is used to add a polygon to the current path. * Used by drawPolygon(), drawPolyline() and fillPolygon() etal * @param xp Array of x coordinates * @param yp Array of y coordinates * @param np number of points in polygon * @see #drawPolygon * @see #drawPolyline * @see #fillPolygon */ public void polygon(int[] xp,int[] yp,int np) { // newPath() not needed here as moveto does it ;-) moveto(xp[0],yp[0]); for(int i=1;i<np;i++) lineto(xp[i],yp[i]); } /** * @see Graphics2D#rotate(double) */ public void rotate(double theta) { transform.rotate(theta); } /** * @see Graphics2D#rotate(double, double, double) */ public void rotate(double theta, double x, double y) { transform.rotate(theta, x, y); } /** * @see Graphics2D#scale(double, double) */ public void scale(double sx, double sy) { transform.scale(sx, sy); this.stroke = transformStroke(originalStroke); } /** * @see Graphics2D#setBackground(Color) */ public void setBackground(Color color) { background = color; } /** * Clips to a set of coordinates * @param x coordinate * @param y coordinate * @param w width * @param h height */ public void setClip(int x,int y,int w,int h) { clipRectangle = new Rectangle(x,y,w,h); closeBlock(); // finish off any existing paths drawRect(x,y,w,h); closeBlock("W n"); // clip to current path } /** * As my JDK docs say, this may break with Java 2D. * <p>Sets the clipping region to that of a Shape. * @param s Shape to clip to. */ public void setClip(Shape s) { Rectangle r = s.getBounds(); setClip(r.x,r.y,r.width,r.height); } /** * Sets the color for drawing * @param c Color to use */ public void setColor(Color c) { setPaint(c); } /** * @see Graphics2D#setComposite(Composite) */ public void setComposite(Composite comp) { this.composite = comp; } /** * This extension sets the line width to the default of 1mm which is what * Java uses when drawing to a PrintJob. */ public void setDefaultLineWidth() { closeBlock(); // draw any path before we change the line width pw.println("1 w"); } /** * This sets the font. * @param f java.awt.Font to set to. */ public void setFont(Font f) { // optimize: Save some space if the font is already the current one. if(font!=f) { font = f; pdffont = page.getFont("/Type1",f.getName(),f.getStyle()); // mark the font as changed newFont = true; } } private void setLineCap(int cap) { int lineCap = 0; switch (cap) { case BasicStroke.JOIN_MITER: lineCap = 0; break; case BasicStroke.JOIN_ROUND: lineCap = 1; break; case BasicStroke.JOIN_BEVEL: lineCap = 2; break; } if (this.lineCap != lineCap) { closeBlock(); // draw any path before we change the line width this.lineCap = lineCap; pw.println(""+lineCap+" J"); } } private void setLineJoin(int join) { int lineJoin = 0; switch (join) { case BasicStroke.JOIN_MITER: lineJoin = 0; break; case BasicStroke.JOIN_ROUND: lineJoin = 1; break; case BasicStroke.JOIN_BEVEL: lineJoin = 2; break; } if (this.lineJoin != lineJoin) { closeBlock(); // draw any path before we change the line width this.lineJoin = lineJoin; pw.println(""+lineJoin+" j"); } } /** * This extension allows the width of the drawn line to be set * @param width Line width in pdf graphic units (points) */ public void setLineWidth(float width) { if (width != this.lineWidth) { closeBlock(); // draw any path before we change the line width this.lineWidth = width; pw.println(""+width+" w"); } } private void setMiterLimit(float limit) { if (limit != this.miterLimit) { closeBlock(); // draw any path before we change the line width this.miterLimit = limit; pw.println(""+limit+" M"); } } /** * Sets the paint for drawing * @param paint Paint to use */ public void setPaint(Paint paint) { this.paint = paint; if (paint instanceof Color) { Color c = (Color) paint; double r = ((double) c.getRed()) / 255.0; double g = ((double) c.getGreen()) / 255.0; double b = ((double) c.getBlue()) / 255.0; closeBlock(); // This ensures any paths are drawn in the previous // colours pw.println("" + r + " " + g + " " + b + " rg " + r + " " + g + " " + b + " RG"); } } /** * Not implemented, as this is not supported in the PDF specification. */ public void setPaintMode() { } /** * Sets a rendering hint * @param arg0 * @param arg1 */ public void setRenderingHint(Key arg0, Object arg1) { if (arg1 != null) { rhints.put(arg0, arg1); } else { rhints.remove(arg0); } } // Add Graphics2D methods. /** * @see Graphics2D#setRenderingHints(Map) */ public void setRenderingHints(Map<?,?> hints) { rhints.clear(); rhints.putAll(hints); } /** * @see Graphics2D#setStroke(Stroke) */ public void setStroke(Stroke s) { originalStroke = s; this.stroke = transformStroke(s); if (stroke instanceof BasicStroke) { BasicStroke bs = (BasicStroke) stroke; setLineCap(bs.getEndCap()); setLineJoin(bs.getLineJoin()); setLineWidth(bs.getLineWidth()); setMiterLimit(bs.getMiterLimit()); // TODO: Line dash pattern } } /** * @see Graphics2D#setTransform(AffineTransform) */ public void setTransform(AffineTransform t) { // Save copy of original transform. oTransform = t; // Working copy of transform transform = new AffineTransform(t); // Concatenate Java Space to PDF Space transform transform.concatenate(pTransform); this.stroke = transformStroke(originalStroke); } /** * Not implemented, as this is not supported in the PDF specification. * @param c1 Color to xor with */ public void setXORMode(Color c1) { } //============ Text operations ======================= /** * @see Graphics2D#shear(double, double) */ public void shear(double shx, double shy) { transform.shear(shx, shy); } /** * @see Graphics2D#transform(AffineTransform) */ public void transform(AffineTransform tx) { transform.concatenate(tx); this.stroke = transformStroke(originalStroke); } private Stroke transformStroke(Stroke stroke) { if (!(stroke instanceof BasicStroke)) return stroke; BasicStroke st = (BasicStroke)stroke; float scale = (float)Math.sqrt(Math.abs(transform.getDeterminant())); float dash[] = st.getDashArray(); if (dash != null) { for (int k = 0; k < dash.length; ++k) dash[k] *= scale; } return new BasicStroke(st.getLineWidth() * scale, st.getEndCap(), st.getLineJoin(), st.getMiterLimit(), dash, st.getDashPhase() * scale); } /** * @see Graphics2D#translate(double, double) */ public void translate(double tx, double ty) { transform.translate(tx, ty); trax = (float) tx; tray = (float) ty; } /** * @see Graphics#translate(int, int) */ public void translate(int x, int y) { translate((double)x, (double)y); } /** * Converts the Java space coordinates into pdf text space. * @param x coordinate * @param y coordinate * @param tx coordinate * @param ty coordinate * @return String containing the coordinates in PDF text space */ private String twh(float x,float y,float tx,float ty) { float nx=x, ny=y; float ntx=tx, nty=ty; int mh = (int) page.getPageFormat().getHeight(); int sx=1,sy=1; // switch(mediaRot) // { // case PageFormat.PORTRAIT: // Portrait //nx = x; ny = mh - y; nty = mh - ty; // break; // // case PageFormat.LANDSCAPE: // // Landscape // //nx = y; // //ny = x; // //ntx = ty; // //nty = tx; // //sy=-1; // nx = x; // ny = -y; // ntx = tx; // nty = -ty; // //sy=-1; // break; // // case 180: // // Inverse Portrait // // to be completed // nx = mw - x; // //ny = y; // break; // // case PageFormat.REVERSE_LANDSCAPE: // // Seascape // // to be completed // nx = mw - y; // ny = mh - x; // break; // } nx = sx*(nx-ntx); ny = sy*(ny-nty); return ""+df.format(nx)+" "+df.format(ny)+" "; } /** * Converts the Java space coordinates into pdf text space. * @param x coordinate * @param y coordinate * @return String containing the coordinates in PDF text space */ private String txy(float x,float y) { // float nx=x, ny=y; // int mh = (int) page.getPageFormat().getHeight(); Point2D ptSrc = new Point2D.Float(x, y); Point2D ptDst = new Point2D.Float(); transform.transform(ptSrc, ptDst); // // handle any translations // x+=trax; // y+=tray; // // nx = x; // ny = mh - y; // // System.out.println("\ntxy(" + ptSrc.getX() + ", " + ptSrc.getY() + ")"); // System.out.println("Old [" + nx + "," + ny + "]"); // System.out.println("Trn [" + ptDst.getX() + ", " + ptDst.getY() + "]"); // // return ""+df.format(nx)+" "+df.format(ny)+" "; return ""+df.format(ptDst.getX())+" "+df.format(ptDst.getY())+" "; } } // end class PDFGraphics