/* ******************************************************************************
* Copyright (c) 2006-2013 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.de.erichseifert.vectorgraphics2d;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Image;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import org.xmind.de.erichseifert.vectorgraphics2d.util.DataUtils;
import org.xmind.de.erichseifert.vectorgraphics2d.util.GraphicsUtils;
/**
* {@code Graphics2D} implementation that saves all operations to a string in
* the <i>Portable Document Format</i> (PDF).
*
* @author Jason Wong
*/
public class PDFGraphics2D extends VectorGraphics2D {
/** Prefix string for PDF font resource ids. */
protected static final String FONT_RESOURCE_PREFIX = "F"; //$NON-NLS-1$
/** Prefix string for PDF image resource ids. */
protected static final String IMAGE_RESOURCE_PREFIX = "Im"; //$NON-NLS-1$
/** Prefix string for PDF transparency resource ids. */
protected static final String TRANSPARENCY_RESOURCE_PREFIX = "T"; //$NON-NLS-1$
/**
* Constant to convert values from millimeters to PostScript®/PDF units
* (1/72th inch).
*/
protected static final double MM_IN_UNITS = 72.0 / 25.4;
/** Mapping of stroke endcap values from Java to PDF. */
private static final Map<Integer, Integer> STROKE_ENDCAPS = DataUtils.map(
new Integer[] { BasicStroke.CAP_BUTT, BasicStroke.CAP_ROUND,
BasicStroke.CAP_SQUARE }, new Integer[] { 0, 1, 2 });
/** Mapping of line join values for path drawing from Java to PDF. */
private static final Map<Integer, Integer> STROKE_LINEJOIN = DataUtils.map(
new Integer[] { BasicStroke.JOIN_MITER, BasicStroke.JOIN_ROUND,
BasicStroke.JOIN_BEVEL }, new Integer[] { 0, 1, 2 });
/** Id of the current PDF object. */
private int curObjId;
/** Mapping from objects to file positions. */
private final Map<Integer, Integer> objPositions;
/** Mapping from transparency levels to transparency resource ids. */
private final Map<Double, String> transpResources;
/** Mapping from image data to image resource ids. */
private final Map<BufferedImage, String> imageResources;
/** Mapping from font objects to font resource ids. */
private final Map<Font, String> fontResources;
/** File position of the actual content. */
private int contentStart;
private double scale;
private double scaledOffsetX;
private double scaledOffsetY;
/**
* Constructor that initializes a new {@code PDFGraphics2D} instance. The
* document dimension must be specified as parameters.
*/
public PDFGraphics2D(double x, double y, double width, double height,
double scale, double scaledOffsetX, double scaledOffsetY) {
super(x, y, width, height);
this.scale = scale;
this.scaledOffsetX = Math.round(scaledOffsetX * 100) / 100.0;
this.scaledOffsetY = Math.round(scaledOffsetY * 100) / 100.0;
curObjId = 1;
objPositions = new TreeMap<Integer, Integer>();
transpResources = new TreeMap<Double, String>();
imageResources = new LinkedHashMap<BufferedImage, String>();
fontResources = new LinkedHashMap<Font, String>();
writeHeader();
}
@SuppressWarnings("nls")
@Override
protected void writeString(String str, double x, double y) {
// Escape string
str = str.replaceAll("\\\\", "\\\\\\\\").replaceAll("\t", "\\\\t")
.replaceAll("\b", "\\\\b").replaceAll("\f", "\\\\f")
.replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)");
float fontSize = getFont().getSize2D();
float leading = getFont().getLineMetrics("", getFontRenderContext())
.getLeading();
// Start text and save current graphics state
writeln("q BT");
String fontResourceId = getFontResource(getFont());
writeln("/", fontResourceId, " ", fontSize, " Tf");
// Set leading
writeln(fontSize + leading, " TL");
// Undo swapping of y axis for text
writeln("1 0 0 -1 ", x, " ", y, " cm");
/*
* // Extract lines String[] lines = str.replaceAll("\r\n",
* "\n").replaceAll("\r", "\n").split("\n"); // Paint lines for (int i =
* 0; i < lines.length; i++) { writeln("(", lines[i], ") ", (i == 0) ?
* "Tj" : "'"); }
*/
str = str.replaceAll("[\r\n]", "");
writeln("(", str, ") Tj");
// End text and restore previous graphics state
writeln("ET Q");
}
@SuppressWarnings("nls")
@Override
public void setStroke(Stroke s) {
super.setStroke(s);
if (s instanceof BasicStroke) {
BasicStroke bs = (BasicStroke) s;
writeln(bs.getLineWidth(), " w");
writeln(STROKE_LINEJOIN.get(bs.getLineJoin()), " j");
writeln(STROKE_ENDCAPS.get(bs.getEndCap()), " J");
writeln("[", DataUtils.join(" ", bs.getDashArray()), "] ",
bs.getDashPhase(), " d");
}
}
@SuppressWarnings("nls")
@Override
protected void writeImage(Image img, int imgWidth, int imgHeight, double x,
double y, double width, double height) {
BufferedImage bufferedImg = GraphicsUtils.toBufferedImage(img);
String imageResourceId = getImageResource(bufferedImg);
// Save graphics state
write("q ");
// Take current transformations into account
AffineTransform txCurrent = getTransform();
if (!txCurrent.isIdentity()) {
double[] matrix = new double[6];
txCurrent.getMatrix(matrix);
write(DataUtils.join(" ", matrix), " cm ");
}
// Move image to correct position and scale it to (width, height)
write(width, " 0 0 ", height, " ", x - 2, " ", y - 2, " cm ");
// Swap y axis
write("1 0 0 -1 0 1 cm ");
// Draw image
write("/", imageResourceId, " Do ");
// Restore old graphics state
writeln("Q");
}
@SuppressWarnings("nls")
@Override
public void setColor(Color c) {
if (c != null) {
super.setColor(c);
// Add a new graphics state to resources
double a = c.getAlpha() / 255.0;
String transpResourceId = getTransparencyResource(a);
writeln("/", transpResourceId, " gs");
double r = c.getRed() / 255.0;
double g = c.getGreen() / 255.0;
double b = c.getBlue() / 255.0;
write(r, " ", g, " ", b, " rg ");
writeln(r, " ", g, " ", b, " RG");
}
}
@Override
public void setClip(Shape clip) {
if (getClip() != null) {
writeln("Q");//$NON-NLS-1$
}
super.setClip(clip);
if (getClip() != null) {
writeln("q");//$NON-NLS-1$
writeShape(getClip());
writeln(" W n");//$NON-NLS-1$
}
}
// TODO Correct transformations
/*
* @Override protected void setAffineTransform(AffineTransform tx) { if
* (getTransform().equals(tx)) { return; } // Undo previous transforms if
* (isTransformed()) { writeln("Q"); } // Set new transform
* super.setAffineTransform(tx); // Write transform to document if
* (isTransformed()) { writeln("q"); double[] matrix = new double[6];
* getTransform().getMatrix(matrix); writeln(DataUtils.join(" ", matrix),
* " cm"); } } //
*/
@SuppressWarnings("nls")
@Override
protected void writeHeader() {
Rectangle2D bounds = getBounds();
int x = (int) Math.floor(bounds.getX());// * MM_IN_UNITS);
int y = (int) Math.floor(bounds.getY());// * MM_IN_UNITS);
int w = (int) Math.ceil(bounds.getWidth());// * MM_IN_UNITS);
int h = (int) Math.ceil(bounds.getHeight());// * MM_IN_UNITS);
writeln("%PDF-1.4");
// Object 1
writeObj("Type", "/Catalog", "Pages", "2 0 R");
// Object 2
writeObj("Type", "/Pages", "Kids", "[3 0 R]", "Count", "1");
// Object 3
writeObj("Type", "/Page", "Parent", "2 0 R", "MediaBox",
String.format("[%d %d %d %d]", x, y, w, h), "Contents",
"4 0 R", "Resources", "6 0 R");
// Object 5
writeln(nextObjId(size()), " 0 obj");
writeDict("Length", "5 0 R");
writeln("stream");
contentStart = size();
writeln("q");
// Adjust page size and page origin
writeln(scale, " 0 0 ", -scale, " " + scaledOffsetX + " ", h
- scaledOffsetY, " cm");
}
/**
* Write a PDF dictionary from the specified collection of objects. The
* passed objects are converted to strings. Every object with odd position
* is used as key, every object with even position is used as value.
*
* @param strs
* Objects to be written to dictionary
*/
@SuppressWarnings("nls")
protected void writeDict(Object... strs) {
writeln("<<");
for (int i = 0; i < strs.length; i += 2) {
writeln("/", strs[i], " ", strs[i + 1]);
}
writeln(">>");
}
/**
* Write a collection of elements to the document stream as PDF object. The
* passed objects are converted to strings.
*
* @param strs
* Objects to be written to the document stream.
* @return Id of the PDF object that was written.
*/
protected int writeObj(Object... strs) {
int objId = nextObjId(size());
writeln(objId, " 0 obj"); //$NON-NLS-1$
writeDict(strs);
writeln("endobj"); //$NON-NLS-1$
return objId;
}
/**
* Returns the next PDF object id without incrementing.
*
* @return Next PDF object id.
*/
protected int peekObjId() {
return curObjId + 1;
}
/**
* Returns a new PDF object id with every call.
*
* @param position
* File position of the object.
* @return A new PDF object id.
*/
private int nextObjId(int position) {
objPositions.put(curObjId, position);
return curObjId++;
}
/**
* Returns the resource for the specified transparency level.
*
* @param a
* Transparency level.
* @return A new PDF object id.
*/
protected String getTransparencyResource(double a) {
String name = transpResources.get(a);
if (name == null) {
name = String.format("%s%d", TRANSPARENCY_RESOURCE_PREFIX, //$NON-NLS-1$
transpResources.size() + 1);
transpResources.put(a, name);
}
return name;
}
/**
* Returns the resource for the specified image data.
*
* @param bufferedImg
* Image object with data.
* @return A new PDF object id.
*/
protected String getImageResource(BufferedImage bufferedImg) {
String name = imageResources.get(bufferedImg);
if (name == null) {
name = String.format("%s%d", IMAGE_RESOURCE_PREFIX, //$NON-NLS-1$
imageResources.size() + 1);
imageResources.put(bufferedImg, name);
}
return name;
}
/**
* Returns the resource describing the specified font.
*
* @param font
* Font to be described.
* @return A new PDF object id.
*/
protected String getFontResource(Font font) {
String name = fontResources.get(font);
if (name == null) {
name = String.format("%s%d", FONT_RESOURCE_PREFIX, //$NON-NLS-1$
fontResources.size() + 1);
fontResources.put(font, name);
}
return name;
}
/**
* Utility method for writing a tag closing fragment for drawing operations.
*/
@Override
protected void writeClosingDraw(Shape s) {
writeln(" S"); //$NON-NLS-1$
}
/**
* Utility method for writing a tag closing fragment for filling operations.
*/
@Override
protected void writeClosingFill(Shape s) {
writeln(" f"); //$NON-NLS-1$
if (!(getPaint() instanceof Color)) {
super.writeClosingFill(s);
}
}
/**
* Utility method for writing an arbitrary shape to. It tries to translate
* Java2D shapes to the corresponding PDF shape commands.
*/
@SuppressWarnings("nls")
@Override
protected void writeShape(Shape s) {
// TODO Correct transformations
/*
* if (s instanceof Line2D) { Line2D l = (Line2D) s; double x1 =
* l.getX1(); double y1 = l.getY1(); double x2 = l.getX2(); double y2 =
* l.getY2(); write(x1, " ", y1, " m ", x2, " ", y2, " l"); } else if (s
* instanceof Rectangle2D) { Rectangle2D r = (Rectangle2D) s; double x =
* r.getX(); double y = r.getY(); double width = r.getWidth(); double
* height = r.getHeight(); write(x, " ", y, " ", width, " ", height,
* " re"); } else //
*/
{
s = getTransform().createTransformedShape(s);
PathIterator segments = s.getPathIterator(null);
double[] coordsCur = new double[6];
double[] pointPrev = new double[2];
for (int i = 0; !segments.isDone(); i++, segments.next()) {
if (i > 0) {
write(" ");
}
int segmentType = segments.currentSegment(coordsCur);
switch (segmentType) {
case PathIterator.SEG_MOVETO:
write(coordsCur[0], " ", coordsCur[1], " m");
pointPrev[0] = coordsCur[0];
pointPrev[1] = coordsCur[1];
break;
case PathIterator.SEG_LINETO:
write(coordsCur[0], " ", coordsCur[1], " l");
pointPrev[0] = coordsCur[0];
pointPrev[1] = coordsCur[1];
break;
case PathIterator.SEG_CUBICTO:
write(coordsCur[0], " ", coordsCur[1], " ", coordsCur[2],
" ", coordsCur[3], " ", coordsCur[4], " ",
coordsCur[5], " c");
pointPrev[0] = coordsCur[4];
pointPrev[1] = coordsCur[5];
break;
case PathIterator.SEG_QUADTO:
double x1 = pointPrev[0] + 2.0 / 3.0
* (coordsCur[0] - pointPrev[0]);
double y1 = pointPrev[1] + 2.0 / 3.0
* (coordsCur[1] - pointPrev[1]);
double x2 = coordsCur[0] + 1.0 / 3.0
* (coordsCur[2] - coordsCur[0]);
double y2 = coordsCur[1] + 1.0 / 3.0
* (coordsCur[3] - coordsCur[1]);
double x3 = coordsCur[2];
double y3 = coordsCur[3];
write(x1, " ", y1, " ", x2, " ", y2, " ", x3, " ", y3, " c");
pointPrev[0] = x3;
pointPrev[1] = y3;
break;
case PathIterator.SEG_CLOSE:
write("h");
break;
default:
throw new IllegalStateException("Unknown path operation.");
}
}
}
}
/**
* Returns a string which represents the data of the specified image.
*
* @param bufferedImg
* Image to convert.
* @return String representation of image in PDF hexadecimal format.
*/
private String getPdf(BufferedImage bufferedImg) {
int width = bufferedImg.getWidth();
int height = bufferedImg.getHeight();
int bands = bufferedImg.getSampleModel().getNumBands();
StringBuffer str = new StringBuffer(width * height * bands * 2);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int pixel = bufferedImg.getRGB(x, y) & 0xffffff;
if (bands >= 3) {
String hex = String.format("%06x", pixel); //$NON-NLS-1$
str.append(hex);
} else if (bands == 1) {
str.append(String.format("%02x", pixel)); //$NON-NLS-1$
}
}
str.append('\n');
}
return str.append('>').toString();
}
@SuppressWarnings("nls")
@Override
protected String getFooter() {
StringBuffer footer = new StringBuffer();
// TODO Correct transformations
/*
* if (isTransformed()) { footer.append("Q\n"); }
*/
if (getClip() != null) {
footer.append("Q\n"); //$NON-NLS-1$
}
footer.append("Q"); //$NON-NLS-1$
int contentEnd = size() + footer.length();
footer.append('\n');
footer.append("endstream\n"); //$NON-NLS-1$
footer.append("endobj\n");
int lenObjId = nextObjId(size() + footer.length());
footer.append(lenObjId).append(" 0 obj\n");
footer.append(contentEnd - contentStart).append('\n');
footer.append("endobj\n");
int resourcesObjId = nextObjId(size() + footer.length());
footer.append(resourcesObjId).append(" 0 obj\n");
footer.append("<<\n");
footer.append(" /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n");
// Add resources for fonts
if (!fontResources.isEmpty()) {
footer.append(" /Font <<\n");
for (Map.Entry<Font, String> entry : fontResources.entrySet()) {
Font font = entry.getKey();
String resourceId = entry.getValue();
footer.append(" /").append(resourceId)
.append(" << /Type /Font").append(" /Subtype /")
.append("TrueType").append(" /BaseFont /")
.append(font.getPSName()).append(" >>\n");
}
footer.append(" >>\n");
}
// Add resources for images
if (!imageResources.isEmpty()) {
footer.append(" /XObject <<\n");
int objIdOffset = 0;
for (Map.Entry<BufferedImage, String> entry : imageResources
.entrySet()) {
String resourceId = entry.getValue();
footer.append(" /").append(resourceId).append(' ')
.append(curObjId + objIdOffset).append(" 0 R\n");
objIdOffset++;
}
footer.append(" >>\n");
}
// Add resources for transparency levels
if (!transpResources.isEmpty()) {
footer.append(" /ExtGState <<\n");
for (Map.Entry<Double, String> entry : transpResources.entrySet()) {
Double alpha = entry.getKey();
String resourceId = entry.getValue();
footer.append(" /").append(resourceId)
.append(" << /Type /ExtGState").append(" /ca ")
.append(alpha).append(" /CA ").append(alpha)
.append(" >>\n");
}
footer.append(" >>\n");
}
footer.append(">>\n");
footer.append("endobj\n");
// Add data of images
for (BufferedImage image : imageResources.keySet()) {
int imageObjId = nextObjId(size() + footer.length());
footer.append(imageObjId).append(" 0 obj\n");
footer.append("<<\n");
String imageData = getPdf(image);
footer.append("/Type /XObject\n").append("/Subtype /Image\n")
.append("/Width ").append(image.getWidth()).append('\n')
.append("/Height ").append(image.getHeight()).append('\n')
.append("/ColorSpace /DeviceRGB\n")
.append("/BitsPerComponent 8\n").append("/Length ")
.append(imageData.length()).append('\n')
.append("/Filter /ASCIIHexDecode\n").append(">>\n")
.append("stream\n").append(imageData)
.append("\nendstream\n").append("endobj\n");
}
int objs = objPositions.size() + 1;
int xrefPos = size() + footer.length();
footer.append("xref\n");
footer.append("0 ").append(objs).append('\n');
// lines of xref entries must must be exactly 20 bytes long
// (including line break) and thus end with <SPACE NEWLINE>
footer.append(String.format("%010d %05d", 0, 65535)).append(" f \n");
for (int pos : objPositions.values()) {
footer.append(String.format("%010d %05d", pos, 0)).append(" n \n");
}
footer.append("trailer\n");
footer.append("<<\n");
footer.append("/Size ").append(objs).append('\n');
footer.append("/Root 1 0 R\n");
footer.append(">>\n");
footer.append("startxref\n");
footer.append(xrefPos).append('\n');
footer.append("%%EOF\n");
return footer.toString();
}
@SuppressWarnings("nls")
public void writeFooter(Writer out) throws IOException {
StringBuffer footer = new StringBuffer();
if (getClip() != null) {
footer.append("Q\n");
}
footer.append("Q");
int contentEnd = size() + footer.length();
footer.append('\n');
footer.append("endstream\n");
footer.append("endobj\n");
int lenObjId = nextObjId(size() + footer.length());
footer.append(lenObjId).append(" 0 obj\n");
footer.append(contentEnd - contentStart).append('\n');
footer.append("endobj\n");
int resourcesObjId = nextObjId(size() + footer.length());
footer.append(resourcesObjId).append(" 0 obj\n");
footer.append("<<\n");
footer.append(" /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n");
out.write(footer.toString());
footer = new StringBuffer();
// Add resources for fonts
if (!fontResources.isEmpty()) {
footer.append(" /Font <<\n");
for (Map.Entry<Font, String> entry : fontResources.entrySet()) {
Font font = entry.getKey();
String resourceId = entry.getValue();
footer.append(" /").append(resourceId)
.append(" << /Type /Font").append(" /Subtype /")
.append("TrueType").append(" /BaseFont /")
.append(font.getPSName()).append(" >>\n");
}
footer.append(" >>\n");
}
// Add resources for images
if (!imageResources.isEmpty()) {
footer.append(" /XObject <<\n");
int objIdOffset = 0;
for (Map.Entry<BufferedImage, String> entry : imageResources
.entrySet()) {
String resourceId = entry.getValue();
footer.append(" /").append(resourceId).append(' ')
.append(curObjId + objIdOffset).append(" 0 R\n");
objIdOffset++;
}
footer.append(" >>\n");
}
// Add resources for transparency levels
if (!transpResources.isEmpty()) {
footer.append(" /ExtGState <<\n");
for (Map.Entry<Double, String> entry : transpResources.entrySet()) {
Double alpha = entry.getKey();
String resourceId = entry.getValue();
footer.append(" /").append(resourceId)
.append(" << /Type /ExtGState").append(" /ca ")
.append(alpha).append(" /CA ").append(alpha)
.append(" >>\n");
}
footer.append(" >>\n");
}
footer.append(">>\n");
footer.append("endobj\n");
out.write(footer.toString());
footer = new StringBuffer();
// Add data of images
for (BufferedImage image : imageResources.keySet()) {
int imageObjId = nextObjId(size() + footer.length());
footer.append(imageObjId).append(" 0 obj\n");
footer.append("<<\n");
String imageData = getPdf(image);
footer.append("/Type /XObject\n").append("/Subtype /Image\n")
.append("/Width ").append(image.getWidth()).append('\n')
.append("/Height ").append(image.getHeight()).append('\n')
.append("/ColorSpace /DeviceRGB\n")
.append("/BitsPerComponent 8\n").append("/Length ")
.append(imageData.length()).append('\n')
.append("/Filter /ASCIIHexDecode\n").append(">>\n")
.append("stream\n");
out.write(footer.toString());
out.write(imageData);
out.write("\nendstream\n");
out.write("endobj\n");
footer = new StringBuffer();
}
int objs = objPositions.size() + 1;
int xrefPos = size() + footer.length();
footer.append("xref\n");
footer.append("0 ").append(objs).append('\n');
// lines of xref entries must must be exactly 20 bytes long
// (including line break) and thus end with <SPACE NEWLINE>
footer.append(String.format("%010d %05d", 0, 65535)).append(" f \n");
for (int pos : objPositions.values()) {
footer.append(String.format("%010d %05d", pos, 0)).append(" n \n");
}
footer.append("trailer\n");
footer.append("<<\n");
footer.append("/Size ").append(objs).append('\n');
footer.append("/Root 1 0 R\n");
footer.append(">>\n");
footer.append("startxref\n");
footer.append(xrefPos).append('\n');
footer.append("%%EOF\n");
out.write(footer.toString());
}
@Override
public String toString() {
// String doc = super.toString();
String doc = super.getDocument().toString();
// doc =
// doc.replaceAll("q\n[0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* cm\nQ\n",
// "");
return doc;
}
@Override
public byte[] getBytes() {
try {
return toString().getBytes("UTF-8"); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
return super.getBytes();
}
}
}