Wednesday, August 25, 2010

Flex to Java to PDF with iText and Linear Algebra

So I had a project that required some graphics programing in Adobe Flex and for those graphics to be exported to a PDF and of course a much higher resolution to be used in print. To do this I used the iText library for Java! It truly is a great piece of software and it's free! I won't spend time here explaining how to program in ActionScript or Java or how to transfer data between the two. This also isn't a beginners tutorial on iText. There is a great book on iText. It's called iText in Action. What I'll spend time doing here is explain a bit about how matrices are handled in Flash, in Java and how we can use them to transfer graphics data from the flash player to a PDF.

First off, it would be a good idea to review transform matrices. Wikipedia has a great page on them http://en.wikipedia.org/wiki/Transformation_matrix

Next let's understand how ActionScript and Java treat matrices differently. Flash treats matrices like so:

| a c tx |
| b d ty |

Java on the other hand treats them like so:

| a b tx |
| c d ty |

Notice that the b and the c are swapped, which can pretty much drive you crazy if you don't know or don't catch it.....trust me...I know! So when you send your matrices from ActionScript to Java make sure to flip those two elements!

Now on to the fun part. Dealing with the different coordinate systems between Flash and PDF. Flash, like most graphics platforms, uses a top left Cartesian coordinate system. PDF, like most printing platforms, uses a bottom left Cartesian coordinate system. We need to apply some matrix magic to get our desired results. Let's say that we have converted the matrix from flash to be in the same order as the matrix in Java. One matrix will be sent from Flash representing translation, rotation, scale and skew. Skew, I didn't have to deal with in my project so it will not be covered in this post. If there is a demand for it however let me know and I'll look into it.

First thing we need to do to achieve our final AffineMatrix in Java is to factor out scale. We do that by using the following Trig Identity:

sin^2(theta) + cos^2(theta) = 1^2

If the scale is 1 (100%) then doing the math and taking the square root of the left side of this equation should give us 1. Likewise, if the scale is .8 then it will give us .8. We do this in our context by the following:

float xscale = (float) Math.sqrt(a*a + c*c) ;
float yscale = (float) Math.sqrt(b*b + d*d) ;


Then we find theta:

float theta = (float)Math.acos(a/xscale);
if( b < 0)
theta = 2f * (float) Math.PI - theta;
tx and ty do not have to be factor out, but they do need to be converted to points. For a 300 DPI resolution you could use the following method:
public static  float convertToPoints(float pixels)
{
float points = pixels/(300f/ 72f);
points = points*10;
points = Math.round(points);
points = (points/10);
return points;
}
So to get the tx and ty in points you do the following:
tx = convertToPoints( (float) tx   );
ty = convertToPoints((float) ty  );
Now for the creating creating and multiplying the matrices. In matrices the order of multiplication matters so first we are going to scale the object with the following:
AffineTransform stm = new AffineTransform();
stm.setToIdentity();
stm.scale(xscale, yscale);
Then we are going to translate the object to the top left so that are relative to the tx ty values and multiply it by the scale matrix that we created in the step before. Note that we find the y by subtracting the scaled object height from the total height of the PDF:
AffineTransform ztm = new AffineTransform();
ztm.setToIdentity();
ztm.translate(0, pdfHeight-(imageHeight*yscale);
ztm.concatenate(stm);
Next we create a matrix to translate the object to it's desination and multiply it by the matrix from the above step. Note that we translate with a negative ty since in a top left coordinate system positive goes down and in the bottom left coordinate system positive goes up!
AffineTransform ttm = new AffineTransform();
ttm.translate(tx, -ty);
ttm.concatenate(ztm);
Lastly we create a rotation transform and multiply it by the matrix in the previous step. Note that we use a specific AffineTransform method for rotating theta across an anchor point and that anchor point being the tx, ty that was applied to the last matrix:
AffineTransform rtm = new AffineTransform();
rtm.setToIdentity();
rtm.rotate(theta, ttm.getTranslateX(), ttm.getTranslateY()+(imageHeight*yscale));
rtm.concatenate(ttm);
That's it! Now call transform on your PdfContentByte instance and pass the last matrix above (rtm) to the method call before adding the object. Don't forget to wrap all the matrix multiplication in between the following:
PdfContentByteInstance.saveState();

...matrix stuff

PdfContentByteInstance.restoreState();


Drop me a line if there are any questions.