/*
* IllustratorExporter.java
*
* Created on February 16, 2006, 3:46 PM
*
* To change this template, choose Tools | Template Manager
* and open the template in the editor.
*/
package ika.geoexport;
import ika.geo.*;
import ika.gui.PageFormat;
import java.io.*;
import java.awt.geom.*;
import java.awt.*;
import java.text.DecimalFormat;
import java.util.*;
/**
* An exporter for the Adobe Illustrator version 7 file format.
* @author Bernhard Jenny, Institute of Cartography, ETH Zurich
*/
public class IllustratorExporter extends VectorGraphicsExporter {
/* to keep track of current drawing settings. */
private Color currentFillColor;
private Color currentStrokeColor;
private float currentStrokeWidth;
private DecimalFormat coordinateFormat;
/**
* Count the number of layers created.
*/
private int layerCounter = 0;
/**
* Creates a new instance of IllustratorExporter.
*/
public IllustratorExporter() {
coordinateFormat = new DecimalFormat();
coordinateFormat.setGroupingUsed(false);
coordinateFormat.getDecimalFormatSymbols().setDecimalSeparator('.');
coordinateFormat.setDecimalSeparatorAlwaysShown(false);
}
public String getFileFormatName() {
return "Illustrator";
}
public String getFileExtension() {
return "ai";
}
protected void write(GeoSet geoSet, OutputStream outputStream) throws IOException {
PrintWriter writer = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(outputStream)));
this.currentFillColor = null;
this.currentStrokeColor = null;
this.currentStrokeWidth = 0;
// HEADER
// start the header
writer.println("%!PS-Adobe-3.0");
// header comments
// creator / version
writer.println("%%Creator: Adobe Illustrator(TM) 3.2");
// title
if (geoSet.getName() != null) {
writer.println("%%Title: " + stringToPostScript(geoSet.getName().trim()));
}
// creation date and time %%CreationDate: (5/9/96) (3:57 PM)
writer.print("%%CreationDate: ");
Date date = new Date();
java.text.SimpleDateFormat format =
new java.text.SimpleDateFormat("'('M/d/yy') ('HH:mm a')'");
writer.println(format.format(date));
// bounding box
// If a high-resolution bounding box is specified, the limits
// of the bounding box are derived from the high-resolution bounding
// box. To generate the lower left and upper right limits of the low
// resolution bounding box, the high-resolution bounding box llx and
// lly values are rounded down, and urx and ury values are rounded up.
final double wWC = this.pageFormat.getPageWidthWorldCoordinates();
final double hWC = this.pageFormat.getPageHeightWorldCoordinates();
final double w = dimToPageRoundedPx(wWC);
final double h = dimToPageRoundedPx(hWC);
// %%BoundingBox: llx lly urx ury
writer.print("%%BoundingBox: ");
writer.println(0 + " " + 0 + " " + (long) Math.ceil(w) + " " + (long) Math.ceil(h));
// high resolution bounding box %%HiResBoundingBox: llx lly urx ury
writer.print("%%HiResBoundingBox: ");
writer.println(0 + " " + 0 + " " + w + " " + h);
// document preview
writer.println("%AI3_DocumentPreview: None");
// end header
writer.println("%%EndComments");
// PROLOG
writer.println("%%BeginProlog");
writer.println("%%EndProlog");
// SCRIPT SETUP
writer.println("%%BeginSetup");
writer.println("%%EndSetup");
// SCRIPT BODY
// write geometry
this.writeTopLevelGeoSet(geoSet, writer);
// PAGE Trailer
writer.println("%%PageTrailer");
writer.println("gsave annotatepage grestore showpage");
// SCRIPT TRAILER
writer.println("%%Trailer");
// EOF
writer.println("%%EOF");
writer.flush();
}
/**
* Writes a GeoSet to a PrintWriter. Wraps all features in layers.
* Layers are supported since the file format version 5. Only top-level
* layers are supported.
* geoSet The GeoSet to write.
* @writer The destination to write to.
*/
private void writeTopLevelGeoSet(GeoSet geoSet, PrintWriter writer) {
// travel down the tree until we find a GeoSet that contains more
// than just another GeoSet.
while (geoSet.getNumberOfChildren() == 1
&& geoSet.getNumberOfSubSets() == 1) {
geoSet = (GeoSet) geoSet.getGeoObject(0);
}
// flag that remembers whether we are currently writing to a layer
// that was created for top-level objects that are not GeoSets.
boolean usingNonGeoSetLayer = false;
final int objCount = geoSet.getNumberOfChildren();
for (int i = 0; i < objCount; i++) {
GeoObject obj = geoSet.getGeoObject(i);
// only write visible elements
if (obj.isVisible() == false) {
continue;
}
if (obj instanceof GeoSet) {
// close the current non-GeoSet layer
if (usingNonGeoSetLayer) {
usingNonGeoSetLayer = false;
writeLayerEnd(writer);
}
// wrap the GeoSet into a layer
writeLayerStart(obj.getName(), writer);
writeGeoSet((GeoSet) obj, writer);
writeLayerEnd(writer);
} else {
// create a layer if this is a non-GeoSet and there is currently
// no layer.
if (!usingNonGeoSetLayer) {
writeLayerStart(null, writer);
usingNonGeoSetLayer = true;
}
if (obj instanceof GeoPath) {
writeGeoPath((GeoPath) obj, writer);
} else if (obj instanceof GeoPoint) {
writeGeoPoint((GeoPoint) obj, writer);
} else if (obj instanceof GeoText) {
writeGeoText((GeoText) obj, writer);
} else if (obj instanceof GeoImage) {
writeGeoImage((GeoImage) obj, writer);
}
}
}
}
/**
* Write the start of a layer.
* @param layerName The name for the layer. If null or empty, an automatically
* created name will be used.
* @writer The destination to write to.
*/
private void writeLayerStart(String layerName, PrintWriter writer) {
++layerCounter;
writer.println("%AI5_BeginLayer");
writer.println("1 1 1 1 0 0 0 0 0 0 Lb");
if (layerName != null && layerName.trim().length() > 0) {
writer.print(this.stringToPostScript(layerName.trim()));
writer.println(" Ln");
} else {
writer.println("(Layer " + layerCounter + ") Ln");
}
}
/**
* Write the end of a layer.
* @writer The destination to write to.
*/
private void writeLayerEnd(PrintWriter writer) {
writer.println("LB");
writer.println("%AI5_EndLayer--");
}
/** Write a GeoSet and all its children.
*/
private void writeGeoSet(GeoSet geoSet, PrintWriter writer) {
final int nbrObj = geoSet.getNumberOfChildren();
for (int i = 0; i < nbrObj; i++) {
GeoObject obj = geoSet.getGeoObject(i);
// only write visible elements
if (obj.isVisible() == false) {
continue;
}
if (obj instanceof GeoSet) {
writeGeoSet((GeoSet) obj, writer);
} else if (obj instanceof GeoPath) {
writeGeoPath((GeoPath) obj, writer);
} else if (obj instanceof GeoPoint) {
writeGeoPoint((GeoPoint) obj, writer);
} else if (obj instanceof GeoText) {
writeGeoText((GeoText) obj, writer);
} else if (obj instanceof GeoImage) {
writeGeoImage((GeoImage) obj, writer);
}
}
}
/**
* Convert and write the pixels of an image to hexadecimal format.
*/
private void streamImage(GeoImage geoImage, PrintWriter writer) {
// write lines of 60 characters, as Illustrator does.
char[] line = new char[60];
// count the characters in a line
int counter = 0;
java.awt.image.BufferedImage image = geoImage.getBufferedImage();
// loop over all rows and columns in the image
int cols = geoImage.getCols();
int rows = geoImage.getRows();
// ASCII code of 0 and A minus 10
final int i0 = '0';
final int iA = 'A' - 10;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
// getRGB is slow !!! ???
final int rgb = image.getRGB(col, row);
// red
final int r = (rgb >> 16) & 255;
final int r1 = r / 16;
line[counter++] = (char) (r1 + (r1 < 10 ? i0 : iA));
final int r2 = r % 16;
line[counter++] = (char) (r2 + (r2 < 10 ? i0 : iA));
// green
final int g = (rgb >> 8) & 255;
final int g1 = g / 16;
line[counter++] = (char) (g1 + (g1 < 10 ? i0 : iA));
final int g2 = g % 16;
line[counter++] = (char) (g2 + (g2 < 10 ? i0 : iA));
// blue
final int b = rgb & 255;
final int b1 = b / 16;
line[counter++] = (char) (b1 + (b1 < 10 ? i0 : iA));
final int b2 = b % 16;
line[counter++] = (char) (b2 + (b2 < 10 ? i0 : iA));
// write the line if it's full
if (counter == line.length) {
writer.print("%");
writer.println(line);
counter = 0;
}
}
}
// write the rest of the last line
if (counter > 0) {
writer.print("%");
for (int i = 0; i < counter; i++) {
writer.print(line[i]);
}
writer.println();
}
}
/**
* Write a GeoImage:
* This does not work !!! ???
*/
private void writeGeoImage(GeoImage geoImage, PrintWriter writer) {
// write bounding box
Rectangle2D rect = geoImage.getBounds2D(GeoObject.UNDEFINED_SCALE);
this.writeGeoPath(GeoPath.newRect(rect), writer);
// decide whether to write internal (embedded) or external (linked) image
java.net.URL url = geoImage.getURL();
final boolean writeExternalFile = url != null
&& url.getProtocol().equals("file")
&& url.getPath() != null;
// get the dimension of the image
Rectangle2D bounds = geoImage.getBounds2D(GeoObject.UNDEFINED_SCALE);
double w = dimToPageRoundedPx(bounds.getWidth());
double h = dimToPageRoundedPx(bounds.getHeight());
// reset graphics state. Raster images will be filled with the
// current foreground color otherwise. Copied from Illustrator output.
writer.println("0 O"); // no overprinting
writer.println("0 g"); // gray fill color: 0 (black), 1 (white)
writer.println("1 w"); // linewidth 1
// start writing a raster image
writer.println("%AI5_File:");
writer.println("%AI5_BeginRaster");
// write file path for external image file.
if (writeExternalFile) {
// write the file path to the image that is locally stored.
String path = url.getPath();
// replace '/' by ':' on Mac
if (ika.utils.Sys.isMacOSX()) {
String pathSeparator = System.getProperty("file.separator");
path = ika.utils.StringUtils.replace(path, pathSeparator, ":");
}
writer.print(this.stringToPostScript(path));
writer.println(" 0 XG");
}
// rotation and scale
writer.print("[1 0 0 1 ");
// position
writer.print(xToPageRoundedPx(bounds.getMinX()));
writer.print(" ");
writer.print(yToPageRoundedPx(bounds.getMaxY()));
writer.print("]");
// Bounds (lower left and upper right x,y coordinates).
writer.print(" 0 0 ");
writer.print(w);
writer.print(" ");
writer.print(h);
// Size in pixel: width and height (The documentation inverts height
// and width mistakenly).
writer.print(" ");
writer.print(geoImage.getCols());
writer.print(" ");
writer.print(geoImage.getRows());
// number of bits
writer.print(" 8");
// image type
writer.print(" 3"); // Grayscale:1 RGB:3 CMYK:4 // !!! ???
// alpha channel count
writer.print(" 0");
// reserved
writer.print(" 0");
// binary or ascii
writer.print(" 0"); // binary
// image mask
writer.print(" 0"); // opaque
// image operator
if (writeExternalFile) {
writer.println(" XF");
} else {
writer.println(" XI");
// write the image data for images on remote servers or images
// that are not stored on the hard disk
this.streamImage(geoImage, writer);
}
// close writing of the raster image
writer.println("%AI5_EndRaster");
writer.println("F");
this.writeName(geoImage, writer);
}
/**
* Write a GeoText
*/
private void writeGeoText(GeoText geoText, PrintWriter writer) {
FontSymbol symbol = geoText.getFontSymbol();
writePaintingAttributes(Color.black, Color.black,
this.currentStrokeWidth, writer);
// 0 To text object of type point text start
writer.println("0 To");
// text at a point start Tp
writer.print("1 0 0 1 "); // rotation and scale
final double x = geoText.getVisualX(1. / getDisplayMapScale());
final double y = geoText.getVisualY(1. / getDisplayMapScale());
writeCoordinate(x, y, writer);
writer.println("0 Tp");
writer.println("TP");
// text style
// 0 Tr filled text render mode
writer.println("0 Tr");
// use non-zero winding rule
writer.println("0 XR");
// /_fontname size ascent descent Tf font and size
writer.print("/_");
writer.print(this.fontToPSFontName(symbol.getFont()));
writer.print(" ");
writer.print(symbol.getSize());
writer.println(" Tf");
// no raise or fall: write on baseline
writer.println("0 Ts");
// no horizontal text scaling
writer.println("100 Tz");
// no tracking between characters
writer.println("0 Tt");
// use auto kerning
writer.println("1 TA");
// hyphenation language: US English
writer.println("%_ 0 XL");
// setup tabs
writer.println("36 0 Xb");
// end of tab definition
writer.println("XB");
// intents for paragraphs
writer.println("0 0 0 Ti");
// text alignment
// 0�left aligned, 1�center aligned, 2�right aligned,
// 3�justified (right and left)
if (symbol.isCenterHor()) {
writer.println("1 Ta");
} else {
writer.println("0 Ta");
}
// leading = space between lines and paragraphs
writer.println("0 0 Tl");
// actual text
String str = geoText.getText();
str = this.stringToPostScript(str);
writer.print(str);
writer.println(" TX");
// TO text object of type point text end
writer.println("TO");
this.writeName(geoText, writer);
}
/**
* Write a GeoPoint.
*/
private void writeGeoPoint(GeoPoint geoPoint, PrintWriter writer) {
PointSymbol pointSymbol = geoPoint.getPointSymbol();
GeoPath geoPath = pointSymbol.getPointSymbol(this.getDisplayMapScale(),
geoPoint.getX(), geoPoint.getY());
// find out whether the path is compound, i.e. it consists of more than
// one path. A new PathIterator has to be created for this.
final boolean compound = geoPath.isCompound();
GeoPathIterator pi = geoPath.getIterator();
this.writePathIterator(pi, compound, pointSymbol, writer);
this.writeName(geoPoint, writer);
}
/** Write a GeoPath.
*/
private void writeGeoPath(GeoPath geoPath, PrintWriter writer) {
if (geoPath.getDrawingInstructionCount() == 0) {
return;
}
// find out whether the path is compound, i.e. it consists of more than
// one path.
final boolean compound = geoPath.isCompound();
GeoPathIterator pi = geoPath.getIterator();
this.writePathIterator(pi, compound, geoPath.getVectorSymbol(), writer);
this.writeName(geoPath, writer);
}
/** Write a path iterator describing a graphic objects of straight lines,
* Bezier curves, potentially with holes and islands.
* @param pi The GeoPathIterator to write.
* @param compound True if the path consists of multiple compound paths.
* Note: This property cannot be derived from the pi, since there is no way
* to reset a PathIterator after iterating through it.
* @param vectorSymbol The VectorSymbol specifying the appearance of pi.
*/
private void writePathIterator(GeoPathIterator pi,
boolean compound,
VectorSymbol vectorSymbol,
PrintWriter writer) {
final int UNDEF_SEG_TYPE = -999;
// remember the last written segment type and the point position.
int lastSegmentType = UNDEF_SEG_TYPE;
double lastMoveToX = 0;
double lastMoveToY = 0;
double lastEndX = 0;
double lastEndY = 0;
// write colors and stroke width if necessary.
this.writePaintingAttributes(vectorSymbol, writer);
if (compound) {
writer.print("*u\n");
}
// write geometry
do {
final int segmentType = pi.getInstruction();
switch (segmentType) {
case GeoPathModel.CLOSE:
if (lastSegmentType == GeoPathModel.CLOSE
|| lastSegmentType == GeoPathModel.MOVETO
|| lastSegmentType == UNDEF_SEG_TYPE) {
break;
}
// draw straight line to starting point if the last
// segment did not already close the path
if (lastEndX != lastMoveToX || lastEndY != lastMoveToY) {
writeCoordinate(lastMoveToX, lastMoveToY, "L", writer);
}
// paint partial path
this.writeFillStroke(vectorSymbol, true, writer);
break;
case GeoPathModel.LINETO:
lastEndX = pi.getX();
lastEndY = pi.getY();
writeCoordinate(pi.getX(), pi.getY(), "L", writer);
break;
case GeoPathModel.MOVETO:
// paint previous partial path
if (lastSegmentType != GeoPathModel.CLOSE
&& lastSegmentType != GeoPathModel.MOVETO
&& lastSegmentType != UNDEF_SEG_TYPE) {
this.writeFillStroke(vectorSymbol, false, writer);
}
// start defintion of new path
lastMoveToX = pi.getX();
lastMoveToY = pi.getY();
lastEndX = pi.getX();
lastEndY = pi.getY();
this.writeCoordinate(lastMoveToX, lastMoveToY, "m", writer);
break;
case GeoPathModel.QUADCURVETO:
// not implemented: transform to cubic bezier
throw new UnsupportedOperationException("Not yet implemented");
case GeoPathModel.CURVETO:
this.writeCoordinate(pi.getX(), pi.getY(), writer);
this.writeCoordinate(pi.getX2(), pi.getY2(), writer);
this.writeCoordinate(pi.getX3(), pi.getY3(), "C", writer);
lastEndX = pi.getX3();
lastEndY = pi.getY3();
break;
}
lastSegmentType = segmentType;
} while (pi.next());
if (lastSegmentType != GeoPathModel.CLOSE
&& lastSegmentType != GeoPathModel.MOVETO) {
this.writeFillStroke(vectorSymbol, false, writer);
}
// close the compound path
if (compound) {
writer.print("*U\n");
}
}
/**
* Write the name of an object (if it has one). This feature is not officially documented.
* Before version 6, no names have been found in any file. Names are written using "object tags" (XT).
* This XT operator uses the "AIArtName" identifier. Identifiers must be preceded by a slash.
* Names are also written for layers although layers have their own operator to indicate the name.
* This is conformal to what has been found in files written by Illustrator 9.
*/
private void writeName(GeoObject geoObject, PrintWriter writer) {
String name = geoObject.getName();
if (name == null || name.length() == 0) {
return;
}
name = this.stringToPostScript(name);
if (name != null && name.trim().length() > 0) {
writer.print("/AIArtName ");
writer.print(name.trim());
writer.println(" XT");
}
}
/**
* Convert a java string to a PostScript string.
*/
private String stringToPostScript(String str) {
if (str == null) {
return "()";
}
// replace bachslash by double backslash
str = ika.utils.StringUtils.replace(str, "\\", "\\\\");
// replace formatting characters
str = ika.utils.StringUtils.replace(str, "\n", "\\n");
str = ika.utils.StringUtils.replace(str, "\r", "\\r");
str = ika.utils.StringUtils.replace(str, "\t", "\\t");
str = ika.utils.StringUtils.replace(str, "\b", "\\b");
str = ika.utils.StringUtils.replace(str, "\f", "\\f");
// replace parentheses
str = ika.utils.StringUtils.replace(str, "(", "\\(");
str = ika.utils.StringUtils.replace(str, ")", "\\)");
// missing: convert ASCII chars > 127 to base 85 encoding !!! ???
return "(" + str + ")";
}
/**
* Transforms a pair of coordinate to sheet coordinates, and writes them
* using the specified operator.
*/
private void writeCoordinate(double x, double y, String operator, PrintWriter writer) {
writeCoordinate(x, y, writer);
writer.println(operator);
}
/**
* Transforms a pair of coordinate to sheet coordinates, and writes them.
*/
private void writeCoordinate(double x, double y, PrintWriter writer) {
// apply offset and scale
x = xToPageRoundedPx(x);
y = yToPageRoundedPx(y);
writer.print(coordinateFormat.format(x));
writer.print(" ");
writer.print(coordinateFormat.format(y));
writer.print(" ");
}
/** Writes commands to stroke and / or fill the last geometry.
*/
private void writeFillStroke(VectorSymbol symbol, boolean close, PrintWriter writer) {
if (symbol == null) {
return;
}
String paintingOperator;
boolean fill = symbol.isFilled();
boolean stroke = symbol.isStroked();
if (fill && stroke) {
paintingOperator = close ? "b" : "B";
} else if (fill) {
paintingOperator = close ? "f" : "F";
} else if (stroke) {
paintingOperator = close ? "s" : "S";
} else // nothing: invisible element
{
paintingOperator = close ? "n" : "N";
}
writer.println(paintingOperator);
}
/**
* Writes a new graphic state (fill and stroke colors, stroke width). Does
* not write information that has not changed since the last call of this
* method.
*/
private void writePaintingAttributes(VectorSymbol symbol, PrintWriter writer) {
if (symbol == null) {
return;
}
Color newFillColor = symbol.getFillColor();
Color newStrokeColor = symbol.getStrokeColor();
float newStrokeWidth = symbol.getStrokeWidth();
writePaintingAttributes(newFillColor, newStrokeColor, newStrokeWidth, writer);
}
/**
* Writes a new graphic state (fill and stroke colors, stroke width). Does
* not write information that has not changed since the last call of this
* method.
*/
private void writePaintingAttributes(Color newFillColor,
Color newStrokeColor, float newStrokeWidth, PrintWriter writer) {
if (newStrokeWidth <= 0) {
newStrokeWidth = 1;
}
// make sure currentColors have been initialized. This is not the case
// for the first object.
if (this.currentFillColor == null) {
writeFillColor(newFillColor, writer);
this.currentFillColor = newFillColor;
writeStrokeColor(newStrokeColor, writer);
this.currentStrokeColor = newStrokeColor;
writer.println(Float.toString(newStrokeWidth) + " w");
this.currentStrokeWidth = newStrokeWidth;
return;
}
if (newFillColor != null && !newFillColor.equals(this.currentFillColor)) {
writeFillColor(newFillColor, writer);
this.currentFillColor = newFillColor;
}
if (newStrokeColor != null && !newStrokeColor.equals(this.currentStrokeColor)) {
writeStrokeColor(newStrokeColor, writer);
this.currentStrokeColor = newStrokeColor;
}
if (newStrokeWidth >= 0.f && newStrokeWidth != this.currentStrokeWidth) {
writer.println(Float.toString(newStrokeWidth) + " w");
this.currentStrokeWidth = newStrokeWidth;
}
}
/** Write RGB color.
*/
private void writeColor(Color color, PrintWriter writer) {
writer.print(color.getRed() / 255.f);
writer.print(" ");
writer.print(color.getGreen() / 255.f);
writer.print(" ");
writer.print(color.getBlue() / 255.f);
}
/** Write fill color in RGB.
*/
private void writeFillColor(Color color, PrintWriter writer) {
this.writeColor(color, writer);
writer.println(" Xa");
}
/** Write stroke color in RGB.
*/
private void writeStrokeColor(Color color, PrintWriter writer) {
this.writeColor(color, writer);
writer.println(" XA");
}
private String fontToPSFontName(Font font) {
String fontName = font.getPSName();
if ("ArialMS".equals(fontName)) // Illustrator cannot read Microsoft's Arial
{
fontName = "ArialMT";
}
return fontName;
}
/**
* Transforms y coordinate to pixel coordinates.
* Overwrites method because y axis is upwards oriented in Illustrator.
*/
@Override
protected double yToPagePx(double y) {
final double mapScale = this.pageFormat.getPageScale();
final double south = this.pageFormat.getPageBottom();
return (y - south) * 1000. * PageFormat.MM2PX / mapScale;
}
}