/*
 * VectorGraphics2D: Vector export for Java(R) Graphics2D
 *
 * (C) Copyright 2010 Erich Seifert <dev[at]erichseifert.de>
 *
 * This file is part of VectorGraphics2D.
 *
 * VectorGraphics2D 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 3 of the License, or
 * (at your option) any later version.
 *
 * VectorGraphics2D 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 VectorGraphics2D.  If not, see <http://www.gnu.org/licenses/>.
 */

package de.erichseifert.vectorgraphics2d;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.MultipleGradientPaint;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderableImage;
import java.io.UnsupportedEncodingException;
import java.text.AttributedCharacterIterator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * Base for classed that want to implement vector export.
 * @author Erich Seifert
 */
public abstract class VectorGraphics2D extends Graphics2D {
	/** Constants to define how fonts are rendered. */
	public static enum FontRendering {
		/** Constant indicating that fonts should be rendered as
		text objects. */
		TEXT,
		/** Constant indicating that fonts should be converted to vectors. */
		VECTORS
	}
	/** Maximal resolution for image rastering. */
	private static final int DEFAULT_PAINT_IMAGE_SIZE_MAXIMUM = 128;

	/** Document contents. */
	private final StringBuffer document;
	/** Rectangular bounds of the documents. */
	private final Rectangle2D bounds;
	/** Resolution in dots per inch that is used to raster paints. */
	private double resolution;
	/** Maximal size of images that are used to raster paints. */
	private int rasteredImageSizeMaximum;
	/** Font rendering mode. */
	private FontRendering fontRendering;
	/** Flag that stores whether affine transformations have been applied. */
	private boolean transformed;

	/** Rendering hints. */
	private final RenderingHints hints;
	/** Current background color. */
	private Color background;
	/** Current foreground color. */
	private Color color;
	/** Shape used for clipping paint operations. */
	private Shape clip;
	/** Method used for compositing. */
	private Composite composite;
	/** Device configuration settings. */
	private final GraphicsConfiguration deviceConfig;
	/** Current font. */
	private Font font;
	/** Context settings used to render fonts. */
	private final FontRenderContext fontRenderContext;
	/** Paint used to fill shapes. */
	private Paint paint;
	/** Stroke used for drawing shapes. */
	private Stroke stroke;
	/** Current transformation matrix. */
	private final AffineTransform transform;
	/** XOR mode used for rendering. */
	private Color xorMode;

	/**
	 * Constructor to initialize a new {@code VectorGraphics2D} document.
	 * The dimensions of the document must be passed.
	 * @param x Horizontal position of document origin.
	 * @param y Vertical position of document origin.
	 * @param width Width of document.
	 * @param height Height of document.
	 */
	public VectorGraphics2D(double x, double y, double width, double height) {
		hints = new RenderingHints(new HashMap<RenderingHints.Key, Object>());
		document = new StringBuffer();
		bounds = new Rectangle2D.Double(x, y, width, height);
		fontRendering = FontRendering.TEXT;
		resolution = 72.0;
		rasteredImageSizeMaximum = DEFAULT_PAINT_IMAGE_SIZE_MAXIMUM;

		background = Color.WHITE;
		color = Color.BLACK;
		composite = AlphaComposite.getInstance(AlphaComposite.CLEAR);
		deviceConfig = null;
		font = Font.decode(null);
		fontRenderContext = new FontRenderContext(null, false, true);
		paint = color;
		stroke = new BasicStroke(1f);
		transform = new AffineTransform();
		transformed = false;
		xorMode = Color.BLACK;
	}

	@Override
	public void addRenderingHints(Map<?,?> hints) {
		this.hints.putAll(hints);
	}

	@Override
	public void clip(Shape s) {
		if ((getClip() != null) && (s != null)) {
			Area clipAreaOld = new Area(getClip());
			Area clipAreaNew = new Area(s);
			clipAreaNew.intersect(clipAreaOld);
			s = clipAreaNew;
		}
		setClip(s);
	}

	@Override
	public void draw(Shape s) {
		writeShape(s);
		writeClosingDraw(s);
	}

	@Override
	public void drawGlyphVector(GlyphVector g, float x, float y) {
		draw(g.getOutline(x, y));
	}

	@Override
	public boolean drawImage(Image img, AffineTransform xform,
			ImageObserver obs) {
		BufferedImage bimg = getTransformedImage(img, xform);
		drawImage(bimg, null, bimg.getMinX(), bimg.getMinY());
		return true;
	}

	@Override
	public void drawImage(BufferedImage img, BufferedImageOp op,
			int x, int y) {
		if (op != null) {
			img = op.filter(img, null);
		}
		drawImage(img, x, y, img.getWidth(), img.getHeight(), null);
	}

	@Override
	public void drawRenderableImage(RenderableImage img,
			AffineTransform xform) {
		drawRenderedImage(img.createDefaultRendering(), xform);
	}

	@Override
	public void drawRenderedImage(RenderedImage img,
			AffineTransform xform) {
		// TODO Implement
		//throw new UnsupportedOperationException("Rendered images aren't supported.");
	}

	@Override
	public void drawString(String str, int x, int y) {
		drawString(str, (float) x, (float) y);
	}

	@Override
	public void drawString(String str, float x, float y) {
		if (str != null && str.trim().isEmpty()) {
			return;
		}
		switch (getFontRendering()) {
		case VECTORS:
			TextLayout layout = new TextLayout(str, getFont(),
					getFontRenderContext());
			Shape s = layout.getOutline(
					AffineTransform.getTranslateInstance(x, y));
			fill(s);
			break;
		case TEXT:
			writeString(str, x, y);
			break;
		default:
			throw new IllegalStateException("Unknown font rendering mode.");
		}
	}

	@Override
	public void drawString(AttributedCharacterIterator iterator,
			int x, int y) {
		drawString(iterator, (float) x, (float) y);
	}

	@Override
	public void drawString(AttributedCharacterIterator iterator,
			float x, float y) {
		// TODO Take text formatting into account
		StringBuffer buf = new StringBuffer();
		for (char c = iterator.first(); c != AttributedCharacterIterator.DONE;
				c = iterator.next()) {
			buf.append(c);
		}
		drawString(buf.toString(), x, y);
	}

	@Override
	public void fill(Shape s) {
		writeShape(s);
		writeClosingFill(s);
	}

	@Override
	public Color getBackground() {
		return background;
	}

	@Override
	public Composite getComposite() {
		return composite;
	}

	@Override
	public GraphicsConfiguration getDeviceConfiguration() {
		return deviceConfig;
	}

	@Override
	public FontRenderContext getFontRenderContext() {
		return fontRenderContext;
	}

	@Override
	public Paint getPaint() {
		return paint;
	}

	@Override
	public Object getRenderingHint(RenderingHints.Key hintKey) {
		if (RenderingHints.KEY_ANTIALIASING.equals(hintKey)) {
			return RenderingHints.VALUE_ANTIALIAS_OFF;
		} else if (RenderingHints.KEY_TEXT_ANTIALIASING.equals(hintKey)) {
			return RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
		} else if (RenderingHints.KEY_FRACTIONALMETRICS.equals(hintKey)) {
			return RenderingHints.VALUE_FRACTIONALMETRICS_ON;
		}
		return hints.get(hintKey);
	}

	@Override
	public RenderingHints getRenderingHints() {
		return hints;
	}

	@Override
	public Stroke getStroke() {
		return stroke;
	}

	@Override
	public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
		if (onStroke) {
			Shape sStroke = getStroke().createStrokedShape(s);
			return sStroke.intersects(rect);
		} else  {
			return s.intersects(rect);
		}
	}

	@Override
	public void setBackground(Color color) {
		background = color;
	}

	@Override
	public void setComposite(Composite comp) {
		composite = comp;
	}

	@Override
	public void setPaint(Paint paint) {
		if (paint != null) {
			this.paint = paint;
			if (paint instanceof Color) {
				setColor((Color) paint);
			} else if (paint instanceof MultipleGradientPaint) {
				// Set brightest or least opaque color for gradients
				Color[] colors = ((MultipleGradientPaint) paint).getColors();
				if (colors.length == 1) {
					setColor(colors[0]);
				} else if (colors.length > 1) {
					Color colLight = colors[0];
					float brightness = getBrightness(colLight);
					int alpha = colLight.getAlpha();

					for (int i = 1; i < colors.length; i++) {
						Color c = colors[i];
						float b = getBrightness(c);
						int a = c.getAlpha();
						if (b < brightness || a < alpha) {
							colLight = c;
							brightness = b;
						}
					}
					setColor(colLight);
				}
			}
		}
	}

	/**
	 * Utility method to get the brightness of a specified color.
	 * @param c Color.
	 * @return Brightness value between 0f (black) and 1f (white).
	 */
	private static float getBrightness(Color c) {
		return Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null)[2];
	}

	@Override
	public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {
		hints.put(hintKey, hintValue);
	}

	@Override
	public void setRenderingHints(Map<?, ?> hints) {
		this.hints.putAll(hints);
	}

	@Override
	public void setStroke(Stroke s) {
		stroke = s;
	}

	@Override
	public AffineTransform getTransform() {
		return new AffineTransform(transform);
	}

	@Override
	public void setTransform(AffineTransform tx) {
		setAffineTransform(tx);
	}

	/**
	 * Sets the current transformation.
	 * @param tx Current transformation
	 */
	protected void setAffineTransform(AffineTransform tx) {
		if (!transform.equals(tx)) {
			transform.setTransform(tx);
			transformed = true;
		}
	}

	@Override
	public void shear(double shx, double shy) {
		AffineTransform transform = getTransform();
		transform.shear(shx, shy);
		setAffineTransform(transform);
	}

	@Override
	public void transform(AffineTransform tx) {
		AffineTransform transform = getTransform();
		transform.concatenate(tx);
		setAffineTransform(transform);
	}

	@Override
	public void translate(int x, int y) {
		translate((double) x, (double) y);
	}

	@Override
	public void translate(double tx, double ty) {
		AffineTransform transform = getTransform();
		transform.translate(tx, ty);
		setAffineTransform(transform);
	}

	@Override
	public void rotate(double theta) {
		AffineTransform transform = getTransform();
		transform.rotate(theta);
		setAffineTransform(transform);
	}

	@Override
	public void rotate(double theta, double x, double y) {
		AffineTransform transform = getTransform();
		transform.rotate(theta, x, y);
		setAffineTransform(transform);
	}

	@Override
	public void scale(double sx, double sy) {
		AffineTransform transform = getTransform();
		transform.scale(sx, sy);
		setAffineTransform(transform);
	}

	@Override
	public void clearRect(int x, int y, int width, int height) {
		// TODO Implement
		//throw new UnsupportedOperationException("clearRect() isn't supported by VectorGraphics2D.");
	}

	@Override
	public void clipRect(int x, int y, int width, int height) {
		clip(new Rectangle(x, y, width, height));
	}

	@Override
	public void copyArea(int x, int y, int width, int height, int dx, int dy) {
		// TODO Implement
		//throw new UnsupportedOperationException("copyArea() isn't supported by VectorGraphics2D.");
	}

	@Override
	public Graphics create() {
		// TODO Implement
		return this;
	}

	@Override
	public void dispose() {
		// TODO Implement
	}

	@Override
	public void drawArc(int x, int y, int width, int height,
			int startAngle, int arcAngle) {
		draw(new Arc2D.Double(x, y, width, height,
				startAngle, arcAngle, Arc2D.OPEN));
	}

	@Override
	public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
		return drawImage(img, x, y,
				img.getWidth(observer), img.getHeight(observer), observer);
	}

	@Override
	public boolean drawImage(Image img, int x, int y, Color bgcolor,
			ImageObserver observer) {
		return drawImage(img, x, y,
				img.getWidth(observer), img.getHeight(observer), observer);
	}

	@Override
	public boolean drawImage(Image img, int x, int y, int width, int height,
			ImageObserver observer) {
		int imgWidth = img.getWidth(observer);
		int imgHeight = img.getHeight(observer);
		writeImage(img, imgWidth, imgHeight, x, y, width, height);
		return true;  // TODO Return only true if image data was complete
	}

	@Override
	public boolean drawImage(Image img, int x, int y, int width, int height,
			Color bgcolor, ImageObserver observer) {
		return drawImage(img, x, y, width, height, observer);
	}

	@Override
	public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2,
			int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
		if (img == null) {
			return true;
		}

		int sx = Math.min(sx1, sx2);
		int sy = Math.min(sy1, sy2);
		int sw = Math.abs(sx2 - sx1);
		int sh = Math.abs(sy2 - sy1);
		int dx = Math.min(dx1, dx2);
		int dy = Math.min(dy1, dy2);
		int dw = Math.abs(dx2 - dx1);
		int dh = Math.abs(dy2 - dy1);

		// Draw image
		BufferedImage bufferedImg = GraphicsUtils.toBufferedImage(img);
		Image cropped = bufferedImg.getSubimage(sx, sy, sw, sh);
		return drawImage(cropped, dx, dy, dw, dh, observer);
	}

	@Override
	public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2,
			int sx1, int sy1, int sx2, int sy2, Color bgcolor,
			ImageObserver observer) {
		if (img == null) {
			return true;
		}

		int sx = Math.min(sx1, sx2);
		int sy = Math.min(sy1, sy2);
		int sw = Math.abs(sx2 - sx1);
		int sh = Math.abs(sy2 - sy1);
		int dx = Math.min(dx1, dx2);
		int dy = Math.min(dy1, dy2);
		int dw = Math.abs(dx2 - dx1);
		int dh = Math.abs(dy2 - dy1);

		// Fill Rectangle with bgcolor
		Color bgcolorOld = getColor();
		setColor(bgcolor);
		fill(new Rectangle(dx, dy, dw, dh));
		setColor(bgcolorOld);

		// Draw image on rectangle
		BufferedImage bufferedImg = GraphicsUtils.toBufferedImage(img);
		Image cropped = bufferedImg.getSubimage(sx, sy, sw, sh);
		return drawImage(cropped, dx, dy, dw, dh, observer);
	}

	@Override
	public void drawLine(int x1, int y1, int x2, int y2) {
		draw(new Line2D.Double(x1, y1, x2, y2));
	}

	@Override
	public void drawOval(int x, int y, int width, int height) {
		draw(new Ellipse2D.Double(x, y, width, height));
	}

	@Override
	public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
		Path2D p = new Path2D.Float();
		for (int i = 0; i < nPoints; i++) {
			if (i > 0) {
				p.lineTo(xPoints[i], yPoints[i]);
			} else {
				p.moveTo(xPoints[i], yPoints[i]);
			}
		}
		p.closePath();
		draw(p);
	}

	@Override
	public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
		Path2D p = new Path2D.Float();
		for (int i = 0; i < nPoints; i++) {
			if (i > 0) {
				p.lineTo(xPoints[i], yPoints[i]);
			} else {
				p.moveTo(xPoints[i], yPoints[i]);
			}
		}
		draw(p);
	}

	@Override
	public void drawRect(int x, int y, int width, int height) {
		draw(new Rectangle2D.Double(x, y, width, height));
	}

	@Override
	public void drawRoundRect(int x, int y, int width, int height,
			int arcWidth, int arcHeight) {
		draw(new RoundRectangle2D.Double(x, y, width, height,
				arcWidth, arcHeight));
	}

	@Override
	public void fillArc(int x, int y, int width, int height,
			int startAngle, int arcAngle) {
		fill(new Arc2D.Double(x, y, width, height,
				startAngle, arcAngle, Arc2D.PIE));
	}

	@Override
	public void fillOval(int x, int y, int width, int height) {
		fill(new Ellipse2D.Double(x, y, width, height));
	}

	@Override
	public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
		Path2D p = new Path2D.Float();
		for (int i = 0; i < nPoints; i++) {
			if (i > 0) {
				p.lineTo(xPoints[i], yPoints[i]);
			} else {
				p.moveTo(xPoints[i], yPoints[i]);
			}
		}
		p.closePath();

		fill(p);
	}

	@Override
	public void fillRect(int x, int y, int width, int height) {
		fill(new Rectangle2D.Double(x, y, width, height));
	}

	@Override
	public void fillRoundRect(int x, int y, int width, int height,
			int arcWidth, int arcHeight) {
		fill(new RoundRectangle2D.Double(x, y, width, height,
				arcWidth, arcHeight));
	}

	@Override
	public Shape getClip() {
		return clip;
	}

	@Override
	public Rectangle getClipBounds() {
		if (clip == null) {
			return null;
		}
		return clip.getBounds();
	}

	@Override
	public Color getColor() {
		return color;
	}

	@Override
	public Font getFont() {
		return font;
	}

	@Override
	public FontMetrics getFontMetrics(Font f) {
		// TODO Find a better way for creating a new FontMetrics instance
		BufferedImage bi =
			new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB_PRE);
	    Graphics g = bi.getGraphics();
	    FontMetrics fontMetrics = g.getFontMetrics(font);
	    g.dispose();
	    bi = null;
	    return fontMetrics;
	}

	@Override
	public void setClip(Shape clip) {
		this.clip = clip;
	}

	@Override
	public void setClip(int x, int y, int width, int height) {
		setClip(new Rectangle(x, y, width, height));
	}

	@Override
	public void setColor(Color c) {
		color = c;
	}

	@Override
	public void setFont(Font font) {
		if (!this.font.equals(font)) {
			this.font = font;
		}
	}

	@Override
	public void setPaintMode() {
		// TODO Implement
		//throw new UnsupportedOperationException("setPaintMode() isn't supported.");
	}

	@Override
	public void setXORMode(Color c1) {
		xorMode = c1;
	}

	/**
	 * Utility method for writing multiple objects to the SVG document.
	 * @param strs Objects to be written
	 */
	protected void write(Object... strs) {
		for (Object o : strs) {
			String str = o.toString();
			if ((o instanceof Double) || (o instanceof Float)) {
				str = String.format(Locale.ENGLISH, "%.7f", o)
					.replaceAll("\\.?0+$", "");
			}
			document.append(str);
		}
	}

	/**
	 * Utility method for writing a line of multiple objects to the
	 * SVG document.
	 * @param strs Objects to be written
	 */
	protected void writeln(Object... strs) {
		write(strs);
		write("\n");
	}

	/**
	 * Write the specified shape to the document. This does not necessarily
	 * contain the actual command to paint the shape.
	 * @param s Shape to be written.
	 */
	protected abstract void writeShape(Shape s);

	/**
	 * Write the specified image to the document. A number of dimensions will
	 * specify how the image will be placed in the document.
	 * @param img Image to be rendered.
	 * @param imgWidth Number of pixels in horizontal direction.
	 * @param imgHeight Number of pixels in vertical direction
	 * @param x Horizontal position in document units where the
	 *          upper left corner of the image should be placed.
	 * @param y Vertical position in document units where the
	 *          upper left corner of the image should be placed.
	 * @param width Width of the image in document units.
	 * @param height Height of the image in document units.
	 */
	protected abstract void writeImage(Image img, int imgWidth, int imgHeight,
			double x, double y, double width, double height);

	/**
	 * Write a text string to the document at a specified position.
	 * @param str Text to be rendered.
	 * @param x Horizontal position in document units.
	 * @param y Vertical position in document units.
	 */
	protected abstract void writeString(String str, double x, double y);

	/**
	 * Write a command to draw the outline of a previously inserted shape.
	 * @param s Shape that should be drawn.
	 */
	protected abstract void writeClosingDraw(Shape s);

	/**
	 * Write a command to fill the outline of a previously inserted shape.
	 * @param s Shape that should be filled.
	 */
	protected void writeClosingFill(Shape s) {
		Rectangle2D shapeBounds = s.getBounds2D();

		// Calculate dimensions of shape with current transformations
		int wImage = (int) Math.ceil(shapeBounds.getWidth()*getResolution());
		int hImage = (int) Math.ceil(shapeBounds.getHeight()*getResolution());
		// Limit the size of images
		wImage = Math.min(wImage, rasteredImageSizeMaximum);
		hImage = Math.min(hImage, rasteredImageSizeMaximum);

		// Create image to paint draw gradient with current transformations
		BufferedImage paintImage = new BufferedImage(
				wImage, hImage, BufferedImage.TYPE_INT_ARGB);

		// Paint shape
		Graphics2D g = (Graphics2D) paintImage.getGraphics();
		g.scale(wImage/shapeBounds.getWidth(), hImage/shapeBounds.getHeight());
		g.translate(-shapeBounds.getX(), -shapeBounds.getY());
		g.setPaint(getPaint());
		g.fill(s);
		// Free resources
		g.dispose();

		// Output image of gradient
		writeImage(paintImage, wImage, hImage,
			shapeBounds.getX(), shapeBounds.getY(),
			shapeBounds.getWidth(), shapeBounds.getHeight());
	}

	/**
	 * Write the header to start a new document.
	 */
	protected abstract void writeHeader();

	/**
	 * Returns a string of the footer to end a document.
	 */
	protected abstract String getFooter();

	/**
	 * Returns a transformed version of an image.
	 * @param image Image to be transformed
	 * @param xform Affine transform to be applied
	 * @return Image with transformed content
	 */
	private BufferedImage getTransformedImage(Image image,
			AffineTransform xform) {
		Integer interpolationType =
			(Integer) hints.get(RenderingHints.KEY_INTERPOLATION);
		if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
				.equals(interpolationType)) {
			interpolationType = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
		} else if (RenderingHints.VALUE_INTERPOLATION_BILINEAR
				.equals(interpolationType)) {
			interpolationType = AffineTransformOp.TYPE_BILINEAR;
		} else {
			interpolationType = AffineTransformOp.TYPE_BICUBIC;
		}
		AffineTransformOp op = new AffineTransformOp(xform, interpolationType);
		BufferedImage bufferedImage = GraphicsUtils.toBufferedImage(image);
		return op.filter(bufferedImage, null);
	}

	/**
	 * Returns whether a distorting transformation has been applied to the
	 * document.
	 * @return <code>true</code> if the document is distorted,
	 *         otherwise <code>false</code>.
	 */
	protected boolean isDistorted() {
		if (!isTransformed()) {
			return false;
		}
		int type = transform.getType();
		int otherButTranslatedOrScaled = ~(AffineTransform.TYPE_TRANSLATION
				| AffineTransform.TYPE_MASK_SCALE);
		return (type & otherButTranslatedOrScaled) != 0;
	}

	@Override
	public String toString() {
		return document.toString() + getFooter();
	}

	/**
	 * Encodes the painted data into a sequence of bytes.
	 * @return A byte array containing the data in the current file format.
	 */
	public byte[] getBytes() {
		try {
			return toString().getBytes("UTF-8");
		} catch (UnsupportedEncodingException e) {
			return toString().getBytes();
		}
	}

	/**
	 * Returns the dimensions of the document.
	 * @return dimensions of the document.
	 */
	public Rectangle2D getBounds() {
		Rectangle2D b = new Rectangle2D.Double();
		b.setFrame(bounds);
		return b;
	}

	/**
	 * Returns the number of bytes of the document.
	 * @return size of the document in bytes.
	 */
	protected int size() {
		return document.length();
	}

	/**
	 * Returns how fonts should be rendered.
	 * @return Font rendering mode.
	 */
	public FontRendering getFontRendering() {
		return fontRendering;
	}

	/**
	 * Sets how fonts should be rendered. For example, they can be converted
	 * to vector shapes.
	 * @param mode New font rendering mode.
	 */
	public void setFontRendering(FontRendering mode) {
		fontRendering = mode;
	}

	/**
	 * Returns whether an affine transformation like translation, scaling,
	 * rotation or shearing has been applied to this graphics instance.
	 * @return <code>true</code> if the instance has been transformed,
	 *         <code>false</code> otherwise
	 */
	protected boolean isTransformed() {
		return transformed;
	}

	/**
	 * Returns the resolution in pixels per inch.
	 * @return Resolution in pixels per inch.
	 */
	public double getResolution() {
		return resolution;
	}

	/**
	 * Sets the resolution in pixels per inch.
	 * @param resolution New resolution in pixels per inch.
	 */
	public void setResolution(double resolution) {
		if (resolution <= 0.0) {
			throw new IllegalArgumentException(
					"Only positive non-zero values allowed");
		}
		this.resolution = resolution;
	}

	/**
	 * Returns the maximal size of images which are used to raster paints
	 * like e.g. gradients, or patterns. The default value is 128.
	 * @return Current maximal image size in pixels.
	 */
	public int getRasteredImageSizeMaximum() {
		return rasteredImageSizeMaximum;
	}

	/**
	 * Sets the maximal size of images which are used to raster paints
	 * like e.g. gradients, or patterns.
	 * @param paintImageSizeMaximum New maximal image size in pixels.
	 */
	public void setRasteredImageSizeMaximum(int paintImageSizeMaximum) {
		this.rasteredImageSizeMaximum = paintImageSizeMaximum;
	}
}
