I'm using apache poi library to make a ppt file in my java program.
To insert elements, I added elements to a Document object.
the add order is below.
Picture1 -> rectangle1 -> Picture2 -> rectangle2
But, All Pictures is over all rectangles in output ppt file.
How to set z-order of elements such as the order of add?
Well, here is what you can do. The order of adding is, as you see, Rectangle1, Picture, Rectangle2. And the z-order is respected (we can see all Rectangle2, but a Rectangle1 is partly hidden behind the image):
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.poi.hslf.model.PPGraphics2D;
import org.apache.poi.hslf.model.Picture;
import org.apache.poi.hslf.model.ShapeGroup;
import org.apache.poi.hslf.model.Slide;
import org.apache.poi.hslf.usermodel.SlideShow;
public class PowerPointTest {
public static void main( String[] args ) {
SlideShow slideShow = new SlideShow();
Slide slide = slideShow.createSlide();
try {
// Rectangle1 (partly hidden)
fillRectangle( slide, Color.blue, 20, 20, 300, 300 );
// Image
int index = slideShow.addPicture(new File("IMG_8499.jpg"), Picture.JPEG);
Picture picture = new Picture(index);
picture.setAnchor(new Rectangle( 50, 50, 300, 200 ));
slide.addShape(picture);
// Rectangle2 (all visible)
fillRectangle( slide, Color.yellow, 250, 150, 50, 10 );
FileOutputStream out = new FileOutputStream( "z-order.ppt" );
slideShow.write( out );
out.close();
} catch ( FileNotFoundException e ) {
e.printStackTrace();
} catch ( IOException e ) {
e.printStackTrace();
}
}
private static void fillRectangle( Slide slide, Color color, int x, int y, int width, int height ) {
// Objects are drawn into a shape group, so we need to create one
ShapeGroup group = new ShapeGroup();
// Define position of the drawing inside the slide
Rectangle bounds = new Rectangle(x, y, width, height);
group.setAnchor(bounds);
slide.addShape(group);
// Drawing a rectangle
Graphics2D graphics = new PPGraphics2D(group);
graphics.setColor(color);
graphics.fillRect(x, y, width, height);
}
}
Take a look at this tutorial.
If you need not just to a draw a rectangle using lines, but, for example, adding a table with text cells, see examples here. Hope it helps!
Related
I want to draw an image behind the LineChart so that any grid and lines are drawn over the image. It seems LineChart would need to expose some lower level accessor to the Canvas so this could be done.
public class ChartBgImageTest {
public ChartBgImageTest() {
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis(1, 21, 0.1);
final LineChart<String, Number> lineChart = new LineChart<String, Number>(xAxis, yAxis);
BufferedImage bufferedImage = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration()
.createCompatibleImage(600, 400);
BufferedImage img = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB);
img.createGraphics().fillRect(0, 0, 50, 50);
// Fill img code here
Graphics g2d = bufferedImage.createGraphics();
g2d.drawImage(img, 0, 0, null);
// TODO How to draw the img to lineChart background.
// So that any grids or lines on the chart are drawn above the img.
}
}
The charts in the javafx.scene.chart package don't use a Canvas to draw the charts. They instead use the scene graph. In your question you're creating an image on the fly, but assuming you have an already-created image, possibly as an embedded resource, you could use CSS to add the image to the chart's background. Looking at the XYChart section of JavaFX CSS Reference Guide, you'll see one of the substructures is labeled chart-plot-background and is a Region, which means a background image can be applied to it via CSS. For example:
App.java:
import java.util.Random;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
var chart = new LineChart<>(new NumberAxis(), new NumberAxis(), createChartData());
chart.getXAxis().setLabel("X");
chart.getYAxis().setLabel("Y");
var scene = new Scene(chart, 1000, 650);
scene.getStylesheets().add(getClass().getResource("/style.css").toString());
primaryStage.setScene(scene);
primaryStage.show();
}
private static ObservableList<Series<Number, Number>> createChartData() {
var random = new Random();
var chartData = FXCollections.<Series<Number, Number>>observableArrayList();
for (int i = 1; i <= 3; i++) {
var data = FXCollections.<Data<Number, Number>>observableArrayList();
for (int j = 0; j <= 15; j++) {
data.add(new Data<>(j, random.nextInt(250)));
}
chartData.add(new Series<>("Series #" + i, data));
}
return chartData;
}
}
style.css:
.chart-plot-background {
-fx-background-image: url(/* your URL */);
-fx-background-size: cover;
}
That will only draw the image behind the actual chart content (i.e. the data). The axis, legend, and surrounding space won't have the image behind it. If you want the entire chart to have the background image then you can utilize the fact that Chart, which all chart implementations inherit from, extends from Region. Change the CSS to:
.chart {
-fx-background-image: url(/* your URL */);
-fx-background-size: cover;
}
.chart-plot-background,
.chart-legend {
-fx-background-color: null;
}
If you want something between the plot background and the entire chart you can add the image to .chart-content (documented here).
If you have to stay within code (e.g. because you're creating the image on the fly) then you'll have to get a reference to the necessary Region. To do that you can use Node#lookup(String). Be aware, however, that you may need to wait until the chart has been rendered on screen before calling lookup, as it's possible the needed descendant node has not been created and added to the scene graph beforehand (this is especially true of controls).
import java.util.Random;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.image.Image;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
var chart = new LineChart<>(new NumberAxis(), new NumberAxis(), createChartData());
chart.setTitle("Example Chart");
chart.getXAxis().setLabel("X");
chart.getYAxis().setLabel("Y");
var scene = new Scene(chart, 1000, 650);
primaryStage.setScene(scene);
primaryStage.show();
var plotBackground = (Region) chart.lookup(".chart-plot-background");
plotBackground.setBackground(
new Background(
new BackgroundImage(
new Image(/* your URL */),
null,
null,
BackgroundPosition.CENTER,
new BackgroundSize(0, 0, false, false, true, false))));
}
private static ObservableList<Series<Number, Number>> createChartData() {
var random = new Random();
var chartData = FXCollections.<Series<Number, Number>>observableArrayList();
for (int i = 1; i <= 3; i++) {
var data = FXCollections.<Data<Number, Number>>observableArrayList();
for (int j = 0; j <= 15; j++) {
data.add(new Data<>(j, random.nextInt(250)));
}
chartData.add(new Series<>("Series #" + i, data));
}
return chartData;
}
}
Notice that you can't use BufferedImage directly with the JavaFX API. If you need to actually draw an Image in code you have a few options:
Use a WritableImage directly (via its PixelWriter). This does not provide a nice API, such as fillRect(...).
Draw to a Canvas then create a snapshot of the result.
Use SwingFXUtils#toFXImage(BufferedImage,WritableImage) to convert a BufferedImage into a JavaFX Image
I'm writing an educational application using JavaFX in which the user can draw and manipulate Bezier curves Line, QuadCurve, and CubicCurve. These curves should have the capability to be dragged with mouse. I've got two options available:
1- Using classes Line, QuadCurve, and CubicCurve, and then filling them with transparent color, and stroke them with another color, say black. The problem that arises for this option is that the user wants to drag a curve, but sees that another curve is dragged. The reason for this is that the curve that user is going to drag, resides below another one in the scene graph. For example, in the following figure the smaller curve is not capable of dragging, since it is below the larger one in the scene graph.
2- Using class javafx.scene.shape.Path, in which case the problem is that manipulating such a path is a little bit more complicated, since it's composed of some PathElements, and simply manipulating the elements does not change the Path, unless we remove an element from its elements property, and add a new one. Therefore I prefer approach 1.
How can I Overcome the problem arising in the first approach?
Thank you in advance for your help. A simplified version of my program code is as follows.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.QuadCurve;
import javafx.stage.Stage;
public class SampleForStackOverflow extends Application
{
double lastMouseX;
double lastMouseY;
double lastTranslateX;
double lastTranslateY;
#Override
public void start(Stage window)
{
final double STROKE_WIDTH = 5;
QuadCurve quad = new QuadCurve(100, 200, 150, 50, 200, 200);
quad.setFill(Color.TRANSPARENT);
quad.setStroke(Color.BLACK);
quad.setStrokeWidth(STROKE_WIDTH);
quad.setOnMousePressed(e -> {
lastMouseX = e.getSceneX();
lastMouseY = e.getSceneY();
lastTranslateX = quad.getTranslateX();
lastTranslateY = quad.getTranslateY();
});
quad.setOnMouseDragged(e -> followMouse(quad, e));
CubicCurve cubic = new CubicCurve(0, 300, 100, 0, 300, 0, 300, 300);
cubic.setFill(Color.TRANSPARENT);
cubic.setStroke(Color.BLACK);
cubic.setStrokeWidth(STROKE_WIDTH);
cubic.setOnMousePressed(e -> {
lastMouseX = e.getSceneX();
lastMouseY = e.getSceneY();
lastTranslateX = cubic.getTranslateX();
lastTranslateY = cubic.getTranslateY();
});
cubic.setOnMouseDragged(e -> followMouse(cubic, e));
Group root = new Group(quad, cubic);
Scene scene = new Scene(root, 500, 500);
window.setScene(scene);
window.show();
}
private void followMouse(Node node, MouseEvent e)
{
double deltaX = e.getSceneX() - lastMouseX;
double deltaY = e.getSceneY() - lastMouseY;
node.setTranslateX(deltaX + lastTranslateX);
node.setTranslateY(deltaY + lastTranslateY);
}
public static void main(String[] args) {
launch(args);
}
}
Instead of using a transparent color in your first scenario, you should explicitly set the fill color of your curves to null and set pickOnBounds to false. A transparent color will still catch the mouse events but null will not and when pickOnBounds if false the mouse events will be caught only if you are exactly over the colored parts of your shape.
Creating the QuadCurve and the CubicCurve using Paths seems to work fine for me. Here is a complete example :
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.QuadCurveTo;
import javafx.stage.Stage;
public class SampleForStackOverflow extends Application {
private final double STROKE_WIDTH = 5;
private double lastMouseX;
private double lastMouseY;
private double lastTranslateX;
private double lastTranslateY;
#Override
public void start(Stage window) {
Path quad = initQuadCurve(100, 200, 200, 200, 150, 50);
Path cubic = initCubicCurve(0, 300, 300, 300, 100, 0, 300, 0);
Pane root = new Pane(cubic, quad);
Scene scene = new Scene(root, 500, 500);
window.setScene(scene);
window.show();
}
private Path initQuadCurve(int xStart, int yStart, int xEnd, int yEnd, int controlX, int controlY) {
Path curvePath = new Path();
curvePath.setStrokeWidth(STROKE_WIDTH);
MoveTo moveTo = new MoveTo(xStart, yStart);
QuadCurveTo quadTo = new QuadCurveTo();
quadTo.setControlX(controlX);
quadTo.setControlY(controlY);
quadTo.setX(xEnd);
quadTo.setY(yEnd);
curvePath.getElements().addAll(moveTo, quadTo);
curvePath.setOnMousePressed(e -> {
lastMouseX = e.getSceneX();
lastMouseY = e.getSceneY();
lastTranslateX = curvePath.getTranslateX();
lastTranslateY = curvePath.getTranslateY();
});
curvePath.setOnMouseDragged(e -> followMouse(curvePath, e));
return curvePath;
}
private Path initCubicCurve(int xStart, int yStart, int xEnd, int yEnd, int x1Control, int y1Control, int x2Control,
int y2Control) {
Path curvePath = new Path();
curvePath.setStrokeWidth(STROKE_WIDTH);
MoveTo moveTo = new MoveTo(xStart, yStart);
CubicCurveTo cubicTo = new CubicCurveTo();
cubicTo.setControlX1(x1Control);
cubicTo.setControlY1(y1Control);
cubicTo.setControlX2(x2Control);
cubicTo.setControlY2(y2Control);
cubicTo.setX(xEnd);
cubicTo.setY(yEnd);
curvePath.getElements().addAll(moveTo, cubicTo);
curvePath.setOnMousePressed(e -> {
lastMouseX = e.getSceneX();
lastMouseY = e.getSceneY();
lastTranslateX = curvePath.getTranslateX();
lastTranslateY = curvePath.getTranslateY();
});
curvePath.setOnMouseDragged(e -> followMouse(curvePath, e));
return curvePath;
}
private void followMouse(Node node, MouseEvent e) {
double deltaX = e.getSceneX() - lastMouseX;
double deltaY = e.getSceneY() - lastMouseY;
node.setTranslateX(deltaX + lastTranslateX);
node.setTranslateY(deltaY + lastTranslateY);
}
public static void main(String[] args) {
launch(args);
}
}
To be honest my first attempt was to call quad.setPickOnBounds(false); (and for the cubic as well ) as suggested on both post below :
Mouse Events get Ignored on the Underlying Layer
JavaFX Pass MouseEvents through Transparent Node to Children
But its not working or I miss something, but if creating the path by yourself works find its unnecessary to complicate thing more. In any case I recommend to have a look on the links if you want to follow the first approach. In case you are going to follow the second approach manipulating the paths is not going to be very difficult in my opinion.
I am developing an application in swing which has 5 tabs with following 5 operations on an image :No Operation ,Color Convert ,Affine Transform ,Convolve and Look Up.
Here is the code :
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.LookupOp;
import java.awt.image.LookupTable;
import java.awt.image.ShortLookupTable;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
public class ImageProcessing extends JFrame{
BufferedImage source;
public static void main(String args[])
{
try {
UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
} catch (Exception e1){e1.printStackTrace();}
SwingUtilities.invokeLater(new Runnable(){public void run(){new ImageProcessing();}});
}
public ImageProcessing() {
// TODO Auto-generated constructor stub
super("Image Processing");
setSize(600,400);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
try
{
source=ImageIO.read(new File("src/abc.jpg"));
}catch(IOException e){System.out.println("Exception Here :"+e);}
JTabbedPane jtp=new JTabbedPane();
buildNoOpTab(jtp);
buildAffineTransformOpTab(jtp);
buildColorConvertOpTab(jtp);
buildConvolveOpTab(jtp);
buildLookUpOpTab(jtp);
//buildRescaleOpTab(jtp);
add(jtp);
setVisible(true);
}
private void buildNoOpTab(JTabbedPane jtp)
{
jtp.add("No Op",new JLabel(new ImageIcon(source)));
}
private void buildAffineTransformOpTab(JTabbedPane jtp)
{
BufferedImage dst;
AffineTransform transform=AffineTransform.getScaleInstance(0.5, 0.5);
AffineTransformOp op=new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
dst=op.filter(source, null);
jtp.add("AffineTransform",new JLabel(new ImageIcon(dst)));
}
private void buildColorConvertOpTab(JTabbedPane jtp)
{
BufferedImage dst = null;
ColorSpace clr=ColorSpace.getInstance(ColorSpace.CS_GRAY);
ColorConvertOp op=new ColorConvertOp(clr,null);
dst=op.filter(source,dst);
jtp.add("Color Convert",new JLabel(new ImageIcon(dst)));
}
private void buildConvolveOpTab(JTabbedPane jtp) {
BufferedImage dst = null;
float sharpen[] = new float[] {
0.0f, -1.0f, 0.0f,
-1.0f, 5.0f, -1.0f,
0.0f, -1.0f, 0.0f
};
Kernel kernel = new Kernel(3, 3, sharpen);
ConvolveOp op = new ConvolveOp(kernel);
dst = op.filter(source, null);
jtp.add("Convolve", new JLabel(new ImageIcon(dst)));
}
private void buildLookUpOpTab(JTabbedPane jtp)
{
BufferedImage dst=null;
short[] data=new short[256];
for(int i=0;i<256;i++)
data[i]=(short)(255-i);
LookupTable lkp=new ShortLookupTable(0,data);
LookupOp op=new LookupOp(lkp,null);
dst=op.filter(source, null);
jtp.add("Look Up",new JLabel(new ImageIcon(dst)));
}
}
There is some problem in the buildLookUpOpTab as removing this method application works fine.
Here is the exception which I am getting:
Exception in thread "AWT-EventQueue-0" java.lang.IllegalArgumentException:
Number of color/alpha components should be 3 but length of bits array is 1
at java.awt.image.ColorModel.<init>(ColorModel.java:336)
at java.awt.image.ComponentColorModel.<init>(ComponentColorModel.java:273)
at java.awt.image.LookupOp.createCompatibleDestImage(LookupOp.java:413)
at java.awt.image.LookupOp.filter(LookupOp.java:153)
at ImageProcessing.buildLookUpOpTab(ImageProcessing.java:108)
at ImageProcessing.<init>(ImageProcessing.java:49)
at ImageProcessing$1.run(ImageProcessing.java:30)
at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:251)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:701)
at java.awt.EventQueue.access$000(EventQueue.java:102)
at java.awt.EventQueue$3.run(EventQueue.java:662)
at java.awt.EventQueue$3.run(EventQueue.java:660)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:671)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:244)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:163)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:151)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:147)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:139)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:97)
Can anyone tell me what is the problem in that method?
The LookupOp.filter method says that:
Performs a lookup operation on a BufferedImage. If the color model in the source image is not the same as that in the destination image, the pixels will be converted in the destination. If the destination image is null, a BufferedImage will be created with an appropriate ColorModel. An IllegalArgumentException might be thrown if the number of arrays in the LookupTable does not meet the restrictions stated in the class comment above, or if the source image has an IndexColorModel.
Since you are filtering a BufferedImage created from using ImageIO.read, the color model that the image will have will definitely not be IndexColorModel, since JPEGImageReader (which actually created the BufferdImage from the file) does not support IndexColorModel - in the olden days, JPEGs used the DirectColorModel
Have a look at the answer on this thread on how to read a JPEG file and use a different color model:
Unable to read JPEG image using ImageIO.read(File file)
You need to remove alpha channel from your image before using any filter on it. To make your code working, change:
try
{
source=ImageIO.read(new File("src/abc.jpg"));
} catch(IOException e){System.out.println("Exception Here :"+e);}
with this:
try
{
BufferedImage src = ImageIO.read(new File("abc.jpg"));
int w = src.getWidth();
int h = src.getHeight();
source = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Raster raster = src.getRaster().createChild(0, 0, w, h, 0, 0,
new int[] {0, 1, 2});
source.setData(raster);
}catch(IOException e){System.out.println("Exception Here :"+e);}
The above code creates a new buffered image in RGB mode and set the RGB data of original image to new buffered image ignoring the alpha values. But in case your original image contains completely transparent spots then it will become black spots in new buffered image.
I have created a servlet to handle the save of a JFreeChart displayed in a JSP to a PDF file.
The code I am using so far is:
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.OutputStream;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jfree.chart.JFreeChart;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.DefaultFontMapper;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfTemplate;
import com.lowagie.text.pdf.PdfWriter;
public class ChartPrintServlet extends HttpServlet {
private static final long serialVersionUID = -2445101551756014281L;
protected void doPost ( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
JFreeChart jFreeChart = (JFreeChart) request.getSession().getAttribute("jFreeChart");
String url = "";
int height = 1024;
int width = 1152;
if (jFreeChart == null)
{
url = "/do/error";
RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(url);
dispatcher.forward(request, response);
}
else
{
AbsencesGanttChartPostProcessor postProc = new AbsencesGanttChartPostProcessor();
postProc.processChart(jFreeChart, null);
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=\"absences.pdf\"");
OutputStream out = response.getOutputStream();
try
{
Rectangle pagesize = new Rectangle(width, height);
Document document = new Document(pagesize.rotate(), 30, 30, 30, 30);
PdfWriter writer = PdfWriter.getInstance(document, out);
document.open();
PdfContentByte cb = writer.getDirectContent();
PdfTemplate tp = cb.createTemplate(height, width);
Graphics2D g2 = tp.createGraphics(height, width, new DefaultFontMapper());
Rectangle2D r2D = new Rectangle2D.Double(0, 0, height, width);
jFreeChart.draw(g2, r2D);
g2.dispose();
cb.addTemplate(tp, 0, 0);
document.close();
}
catch (DocumentException de)
{
System.err.println(de.getMessage());
}
finally
{
out.close();
}
}
}
protected void doGet ( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doPost(request, response);
}
}
I have swapped the height and width everywhere in order to get the PDF to look half-decent, but what I really want is to be able to create a PDF as if it were in landscape mode.
If I try
Graphics2D g2 = tp.createGraphics(height, width, new DefaultFontMapper());
g2.rotate(90)
then the PDF just prints a white, blank page.
What is the correct way with the itext / Java awt APIs to go about rotating the entire document (including the JFreeChart underneath) when creating a PDF?
First this: your referring to my name in your code. I'd like you to use itextpdf instead. See http://lowagie.com/itext2
Now for your question. There's an easy way to achieve what you want, and there's a more difficult way.
You only use three parameters in this method: cb.addTemplate(tp, 0, 0); which means that you only want iText to do a translation (zero up, zero to the right). If you also want a rotation, you need to use the method with seven parameters, six of which define the transformation matrix. This is simple algebra; it's explained in my book "iText in Action", but most of the developers I know don't like doing math and they say this is the difficult way.
The easy way, is to wrap tp inside an Image object, and rotate the image:
Image img = Image.getInstance(tp);
img.setRotationDegrees(90);
There's also a setRotation() method that takes radians as a parameter.
Additional notes:
Don't worry about the Image class rasterizing your content. A PdfTemplate wrapped inside an Image object will result in a Form XObject, it won't be changed into an Image XObject.
Be careful not to rotate your image 'outside your page'. You may need to take into account the pivot point.
You rotate your Graphics counterclockwise around the top left corner, and doing so moves everything out of the drawing area. This is why you just get the blank page. You need to apply translation also to shift graphics back into the painting area. Translate y downwards by the width of you image:
g.rotate(Math.PI / 2);
g.translate(0, width);
Also, Graphics2D.rotate expects radians, not degrees.
After that, JFreeChart should draw the transformed chart if you pass transformed Graphics2D for it.
I have an abstract class with an abstract method draw(Graphics2D g2), and the methods print(), showPreview(), printPDF(). For each document in my Java program, I implement draw(), so I can print, show preview and create a PDF file for each document.
My problem is how to create a PDF with multiple pages from that Graphics object.
I solved it by creating a PDF file for each page, and then merge the files into one new file. But there must be a better way.
I have following code to create PDF with one page:
public void printPDF1(){
JFileChooser dialog = new JFileChooser();
String filePath = "";
int dialogResult = dialog.showSaveDialog(null);
if (dialogResult==JFileChooser.APPROVE_OPTION){
filePath = dialog.getSelectedFile().getPath();
}
else return;
try {
Document document = new Document(new Rectangle(_pageWidth, _pageHeight));
PdfWriter writer = PdfWriter.getInstance(document,
new FileOutputStream(filePath));
document.open();
PdfContentByte cb = writer.getDirectContent();
g2 = cb.createGraphics(_pageWidth, _height);
g2.translate(0, (_numberOfPages - _pageNumber) * _pageHeight);
draw(g2);
g2.dispose();
document.close();
}
catch (Exception e2) {
System.out.println(e2.getMessage());
}
}
document.open();
// the same contentByte is returned, it's just flushed & reset during
// new page events.
PdfContentByte cb = writer.getDirectContent();
for (int _pageNumber = 0; _pageNumber < _numberofPages; ++_numberOfPages) {
/*******************/
//harmless in first pass, *necessary* in others
document.newPage();
/*******************/
g2 = cb.createGraphics(_pageWidth, _height);
g2.translate(0, (_numberOfPages - _pageNumber) * _pageHeight);
draw(g2);
g2.dispose();
}
document.close();
So you're rendering your entire interface N times, and only showing a page-sized slice of it in different locations. That's called "striping" in print-world IIRC. Clever, but it could be more efficient in PDF.
Render your entire interface into one huge PdfTemplate (with g2d), once. Then draw that template into all your pages such that the portion you want is visible inside the current page's margins ("media box").
PdfContentByte cb = writer.getDirectContent();
float entireHeight = _numberOfPages * _pageHeight;
PdfTemplate hugeTempl = cb.createTemplate( 0, -entireHeight, pageWidth, _pageHeight );
g2 = hugeTempl.createGraphics(0, -entireHeight, _pageWidth, _pageHeight );
draw(g2);
g2.dispose();
for (int curPg = 0; curPg < _numberOfPages; ++curPg) {
cb.addTemplateSimple( hugeTempl, 0, -_pageHeight * curPg );
document.newPage();
}
PDF's coordinate space sets 0,0 in the lower left corner, and those values increase as you go up and to the right. PdfGraphis2D does a fair amount of magic to hide that difference from you, but we still have to deal with it a bit here... thus the negative coordinates in the bounding box and drawing locations.
This is all "back of the napkin" coding, and it's entirely possible I've made a mistake or two in there... but that's the idea.
I was having trouble following the above code (some of the methods appear to have changed in the current itextpdf version). Here's my solution:
import com.itextpdf.awt.PdfGraphics2D;
import com.itextpdf.text.Document;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfTemplate;
import com.itextpdf.text.pdf.PdfWriter;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.event.WindowEvent;
import java.io.FileOutputStream;
import java.io.OutputStream;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import net.miginfocom.swing.MigLayout;
public class PanelToPDF {
private static JFrame frame= new JFrame();
private static JPanel view= new JPanel();
private static float pageWidth= PageSize.A4.getWidth();
private static float pageHeight= PageSize.A4.getHeight();
public static void main(String[] args) throws Exception {
System.out.println("Page width = " + pageWidth + ", height = " + pageHeight);
initPane();
createMultipagePDF();
frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
}
private static void initPane() {
view.setLayout(new MigLayout());
view.setBackground(Color.WHITE);
for (int i= 1; i <= 160; ++i) {
JLabel label= new JLabel("This is a test! " + i);
label.setForeground(Color.BLACK);
view.add(label, "wrap");
JPanel subPanel= new JPanel();
subPanel.setBackground(Color.RED);
view.add(subPanel);
}
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(new Dimension(Math.round(pageWidth), Math.round(pageHeight)));
frame.add(view);
frame.setVisible(true);
}
private static void createMultipagePDF() throws Exception {
// Calculate the number of pages required. Use the preferred size to get
// the entire panel height, rather than the panel height within the JFrame
int numPages= (int) Math.ceil(view.getPreferredSize().height / pageHeight); // int divided by float
// Output to PDF
OutputStream os= new FileOutputStream("test.pdf");
Document doc= new Document();
PdfWriter writer= PdfWriter.getInstance(doc, os);
doc.open();
PdfContentByte cb= writer.getDirectContent();
// Iterate over pages here
for (int currentPage= 0; currentPage < numPages; ++currentPage) {
doc.newPage(); // not needed for page 1, needed for >1
PdfTemplate template= cb.createTemplate(pageWidth, pageHeight);
Graphics2D g2d= new PdfGraphics2D(template, pageWidth, pageHeight * (currentPage + 1));
view.printAll(g2d);
g2d.dispose();
cb.addTemplate(template, 0, 0);
}
doc.close();
}