/* =====================================================================
* OrsonPDF : a fast, light-weight PDF library for the Java(tm) platform
* =====================================================================
*
* (C)opyright 2013-2015, by Object Refinery Limited. All rights reserved.
*
* Project Info: http://www.object-refinery.com/orsonpdf/index.html
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.]
*
* If you do not wish to be bound by the terms of the GPL, an alternative
* commercial license can be purchased. For details, please see visit the
* Orson PDF home page:
*
* http://www.object-refinery.com/orsonpdf/index.html
*
*/
package com.orsonpdf;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Image;
import java.awt.RadialGradientPaint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import com.orsonpdf.util.Args;
/**
* A {@code Stream} that contains graphics for the PDF document that
* can be generated via the {@link PDFGraphics2D} class. The {@link Page}
* class will create a {@code GraphicsStream} instance to represent its
* content. You won't normally interact directly with this class, it is
* intended that the {@code PDFGraphics2D} class drives the calls to the
* methods of this class.
*/
public class GraphicsStream extends Stream {
/**
* The page the graphics stream belongs to. We need this reference to
* our "parent" so that we can access fonts in the document.
*/
private Page page;
/** The stream content. */
private ByteArrayOutputStream content;
/** The most recent font applied. */
private Font font;
/** The most recent alpha transparency value (in the range 0 to 255). */
private int alpha;
private AffineTransform prevTransInv;
/**
* The decimal formatter for coordinates of geometrical shapes.
*/
private DecimalFormat geometryFormat;
/**
* The decimal formatter for transform matrices.
*/
private DecimalFormat transformFormat;
/**
* Creates a new instance.
*
* @param number the PDF object number.
* @param page the parent page ({@code null} not permitted).
*/
GraphicsStream(int number, Page page) {
super(number);
this.page = page;
this.content = new ByteArrayOutputStream();
this.font = new Font("Dialog", Font.PLAIN, 12);
this.alpha = 255;
// force the formatters to use a '.' for the decimal point
DecimalFormatSymbols dfs = new DecimalFormatSymbols();
dfs.setDecimalSeparator('.');
this.geometryFormat = new DecimalFormat("0.##", dfs);
this.transformFormat = new DecimalFormat("0.######", dfs);
}
private void addContent(String s) {
try {
this.content.write(PDFUtils.toBytes(s));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Pushes the current graphics state onto a stack for later retrieval.
*/
void pushGraphicsState() {
addContent("q\n");
}
/**
* Pops the graphics state that was previously pushed onto the stack.
*/
void popGraphicsState() {
addContent("Q\n");
}
/**
* Applies a graphics transform.
*
* @param t the transform ({@code null} not permitted).
*/
void applyTransform(AffineTransform t) {
StringBuilder b = new StringBuilder();
b.append(transformDP(t.getScaleX())).append(" ");
b.append(transformDP(t.getShearY())).append(" ");
b.append(transformDP(t.getShearX())).append(" ");
b.append(transformDP(t.getScaleY())).append(" ");
b.append(transformDP(t.getTranslateX())).append(" ");
b.append(transformDP(t.getTranslateY())).append(" cm\n");
addContent(b.toString());
}
/**
* Sets the transform.
*
* @param t the transform ({@code null} not permitted).
*/
void setTransform(AffineTransform t) {
AffineTransform tt = new AffineTransform(t);
try {
AffineTransform inv = tt.createInverse();
AffineTransform comb;
if (this.prevTransInv != null) {
comb = new AffineTransform(this.prevTransInv);
comb.concatenate(tt);
} else {
comb = tt;
}
this.prevTransInv = inv;
applyTransform(comb);
} catch (NoninvertibleTransformException e) {
// do nothing
}
}
/**
* Applies a text transform.
*
* @param t the transform ({@code null} not permitted).
*/
void applyTextTransform(AffineTransform t) {
StringBuilder b = new StringBuilder();
b.append(t.getScaleX()).append(" ");
b.append(t.getShearY()).append(" ");
b.append(t.getShearX()).append(" ");
b.append(t.getScaleY()).append(" ");
b.append(t.getTranslateX()).append(" ");
b.append(t.getTranslateY()).append(" Tm\n");
addContent(b.toString());
}
/**
* Applies the specified clip to the current clip.
*
* @param clip the clip ({@code null} not permitted).
*/
void applyClip(Shape clip) {
Args.nullNotPermitted(clip, "clip");
StringBuilder b = new StringBuilder();
Path2D p = new Path2D.Double(clip);
b.append(getPDFPath(p));
b.append("W n\n");
addContent(b.toString());
}
/**
* Applies a stroke. If the stroke is not an instance of
* {@code BasicStroke} this method will do nothing.
*
* @param s the stroke.
*/
void applyStroke(Stroke s) {
if (!(s instanceof BasicStroke)) {
return;
}
BasicStroke bs = (BasicStroke) s;
StringBuilder b = new StringBuilder();
b.append(bs.getLineWidth()).append(" ").append("w\n");
b.append(bs.getEndCap()).append(" J\n");
b.append(bs.getLineJoin()).append(" j\n");
float[] dashArray = bs.getDashArray();
if (dashArray != null) {
b.append(PDFUtils.toPDFArray(dashArray)).append(" 0 d\n");
} else {
b.append("[] 0 d\n");
}
addContent(b.toString());
}
/**
* Applies a color for stroking.
*
* @param c the color ({@code null} not permitted).
*/
void applyStrokeColor(Color c) {
float red = c.getRed() / 255f;
float green = c.getGreen() / 255f;
float blue = c.getBlue() / 255f;
StringBuilder b = new StringBuilder();
b.append(red).append(" ").append(green).append(" ").append(blue)
.append(" RG\n");
addContent(b.toString());
applyAlpha(c.getAlpha());
}
/**
* Applies a color for filling.
*
* @param c the color ({@code null} not permitted).
*/
void applyFillColor(Color c) {
float red = c.getRed() / 255f;
float green = c.getGreen() / 255f;
float blue = c.getBlue() / 255f;
StringBuilder b = new StringBuilder();
b.append(red).append(" ").append(green).append(" ").append(blue)
.append(" rg\n");
addContent(b.toString());
applyAlpha(c.getAlpha());
}
/**
* Applies a {@code GradientPaint} for stroking.
*
* @param gp the gradient paint ({@code null} not permitted).
*/
void applyStrokeGradient(GradientPaint gp) {
// delegate arg checking
String patternName = this.page.findOrCreatePattern(gp);
StringBuilder b = new StringBuilder("/Pattern CS\n");
b.append(patternName).append(" SCN\n");
addContent(b.toString());
}
/**
* Applies a {@code RadialGradientPaint} for stroking.
*
* @param gp the gradient paint ({@code null} not permitted).
*/
void applyStrokeGradient(RadialGradientPaint rgp) {
// delegate arg checking
String patternName = this.page.findOrCreatePattern(rgp);
StringBuilder b = new StringBuilder("/Pattern CS\n");
b.append(patternName).append(" SCN\n");
addContent(b.toString());
}
/**
* Applies a {@code GradientPaint} for filling.
*
* @param gp the gradient paint ({@code null} not permitted).
*/
void applyFillGradient(GradientPaint gp) {
// delegate arg checking
String patternName = this.page.findOrCreatePattern(gp);
StringBuilder b = new StringBuilder("/Pattern cs\n");
b.append(patternName).append(" scn\n");
addContent(b.toString());
}
/**
* Applies a {@code RadialGradientPaint} for filling.
*
* @param gp the gradient paint ({@code null} not permitted).
*/
void applyFillGradient(RadialGradientPaint rgp) {
// delegate arg checking
String patternName = this.page.findOrCreatePattern(rgp);
StringBuilder b = new StringBuilder("/Pattern cs\n");
b.append(patternName).append(" scn\n");
addContent(b.toString());
}
private float alphaFactor = 1.0f;
/**
* Applies the specified alpha composite.
*
* @param alphaComp the alpha composite ({@code null} permitted).
*/
void applyComposite(AlphaComposite alphaComp) {
if (alphaComp == null) {
this.alphaFactor = 1.0f;
} else {
this.alphaFactor = alphaComp.getAlpha();
int a = (int) (alphaComp.getAlpha() * 255f);
if (this.alpha != a) {
String name = this.page.findOrCreateGSDictionary(a);
StringBuilder b = new StringBuilder();
b.append(name).append(" gs\n");
addContent(b.toString());
this.alpha = a;
}
}
}
/**
* Applies the alpha transparency.
*
* @param alpha the new alpha value (in the range {@code 0}
* to {@code 255}).
*/
void applyAlpha(int alpha) {
int a = (int) (alpha * this.alphaFactor);
if (this.alpha != a) {
String name = this.page.findOrCreateGSDictionary(a);
StringBuilder b = new StringBuilder();
b.append(name).append(" gs\n");
addContent(b.toString());
this.alpha = a;
}
}
private String geomDP(double d) {
if (this.geometryFormat != null) {
return geometryFormat.format(d);
} else {
return String.valueOf(d);
}
}
private String transformDP(double d) {
if (this.transformFormat != null) {
return transformFormat.format(d);
} else {
return String.valueOf(d);
}
}
/**
* Draws the specified line.
*
* @param line the line ({@code null} not permitted).
*/
void drawLine(Line2D line) {
StringBuilder b = new StringBuilder();
b.append(geomDP(line.getX1())).append(" ").append(geomDP(line.getY1()))
.append(" ").append("m\n");
b.append(geomDP(line.getX2())).append(" ").append(geomDP(line.getY2()))
.append(" ").append("l\n");
b.append("S\n");
addContent(b.toString());
}
/**
* Draws the specified path.
*
* @param path the path ({@code null} not permitted).
*/
void drawPath2D(Path2D path) {
StringBuilder b = new StringBuilder();
b.append(getPDFPath(path)).append("S\n");
addContent(b.toString());
}
/**
* Fills the specified path.
*
* @param path the path ({@code null} not permitted).
*/
void fillPath2D(Path2D path) {
StringBuilder b = new StringBuilder();
b.append(getPDFPath(path)).append("f\n");
addContent(b.toString());
}
/**
* Applies the specified font (in fact, no change is made to the stream
* until the next call
* to {@link #drawString(java.lang.String, float, float)}).
*
* @param font the font.
*/
void applyFont(Font font) {
this.font = font;
}
/**
* Draws a string at the specified location.
*
* @param text the text.
* @param x the x-coordinate.
* @param y the y-coordinate.
*/
void drawString(String text, float x, float y) {
// we need to get the reference for the current font (creating a
// new font object if there isn't already one)
String fontRef = this.page.findOrCreateFontReference(this.font);
addContent("BT ");
AffineTransform t = new AffineTransform(1.0, 0.0, 0.0, -1.0, 0.0,
y * 2);
applyTextTransform(t);
StringBuilder b = new StringBuilder();
b.append(fontRef).append(" ").append(this.font.getSize())
.append(" Tf ");
b.append(geomDP(x)).append(" ").append(geomDP(y)).append(" Td (")
.append(text).append(") Tj ET\n");
addContent(b.toString());
}
/**
* Draws the specified image into the rectangle {@code (x, y, w, h)}.
*
* @param img the image.
* @param x the x-coordinate of the destination.
* @param y the y-coordinate of the destination.
* @param w the width of the destination.
* @param h the height of the destination.
*/
void drawImage(Image img, int x, int y, int w, int h) {
String imageRef = this.page.addImage(img, true);
StringBuilder b = new StringBuilder();
b.append("q\n");
b.append(geomDP(w)).append(" 0 0 ").append(geomDP(h)).append(" ");
b.append(geomDP(x)).append(" ").append(geomDP(y)).append(" cm\n");
b.append(imageRef).append(" Do\n");
b.append("Q\n");
addContent(b.toString());
}
/**
* A utility method to convert a {@code Path2D} instance to a PDF
* path string.
*
* @param path the path ({@code null} not permitted).
*
* @return The string.
*/
private String getPDFPath(Path2D path) {
StringBuilder b = new StringBuilder();
float[] coords = new float[6];
float lastX = 0;
float lastY = 0;
PathIterator iterator = path.getPathIterator(null);
while (!iterator.isDone()) {
int type = iterator.currentSegment(coords);
switch (type) {
case (PathIterator.SEG_MOVETO):
b.append(geomDP(coords[0])).append(" ");
b.append(geomDP(coords[1])).append(" m\n");
lastX = coords[0];
lastY = coords[1];
break;
case (PathIterator.SEG_LINETO):
b.append(geomDP(coords[0])).append(" ");
b.append(geomDP(coords[1])).append(" l\n");
lastX = coords[0];
lastY = coords[1];
break;
case (PathIterator.SEG_QUADTO):
// PDF doesn't support quadratic bezier curves so we need to
// perform "degree elevation":
// http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline
// /Bezier/bezier-elev.html
float x0 = 0.25f * lastX + 0.75f * coords[0];
float y0 = 0.25f * lastY + 0.75f * coords[1];
float x1 = 0.5f * coords[0] + 0.5f * coords[2];
float y1 = 0.5f * coords[1] + 0.5f * coords[3];
b.append(geomDP(x0)).append(" ");
b.append(geomDP(y0)).append(" ");
b.append(geomDP(x1)).append(" ");
b.append(geomDP(y1)).append(" ");
b.append(geomDP(coords[2])).append(" ");
b.append(geomDP(coords[3])).append(" c\n");
lastX = coords[2];
lastY = coords[3];
break;
case (PathIterator.SEG_CUBICTO):
b.append(geomDP(coords[0])).append(" ");
b.append(geomDP(coords[1])).append(" ");
b.append(geomDP(coords[2])).append(" ");
b.append(geomDP(coords[3])).append(" ");
b.append(geomDP(coords[4])).append(" ");
b.append(geomDP(coords[5])).append(" c\n");
lastX = coords[4];
lastY = coords[5];
break;
case (PathIterator.SEG_CLOSE):
b.append("h\n");
break;
default:
break;
}
iterator.next();
}
return b.toString();
}
@Override
public byte[] getRawStreamData() {
return this.content.toByteArray();
}
}