/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.rendering;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.font.PDCIDFontType0;
import org.apache.pdfbox.pdmodel.font.PDCIDFontType2;
import org.apache.pdfbox.pdmodel.graphics.color.PDPattern;
import org.apache.pdfbox.pdmodel.graphics.image.PDImage;
import org.apache.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern;
import org.apache.pdfbox.pdmodel.graphics.pattern.PDShadingPattern;
import org.apache.pdfbox.pdmodel.graphics.pattern.TilingPaint;
import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
import org.apache.pdfbox.rendering.font.CIDType0Glyph2D;
import org.apache.pdfbox.rendering.font.Glyph2D;
import org.apache.pdfbox.rendering.font.TTFGlyph2D;
import org.apache.pdfbox.rendering.font.Type1Glyph2D;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1CFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern;
import org.apache.pdfbox.pdmodel.graphics.state.PDSoftMask;
import org.apache.pdfbox.pdmodel.graphics.blend.SoftMaskPaint;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern;
import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.util.Matrix;
import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine;
import org.apache.pdfbox.util.Vector;
/**
* Paints a page in a PDF document to a Graphics context.
*
* @author Ben Litchfield
*/
public class PageDrawer extends PDFGraphicsStreamEngine
{
private static final Log LOG = LogFactory.getLog(PageDrawer.class);
// parent document renderer
private final PDFRenderer renderer;
// the graphics device to draw to, xform is the initial transform of the device (i.e. DPI)
private Graphics2D graphics;
private AffineTransform xform;
// the page box to draw (usually the crop box but may be another)
PDRectangle pageSize;
// clipping winding rule used for the clipping path
private int clipWindingRule = -1;
private GeneralPath linePath = new GeneralPath();
// last clipping path
private Area lastClip;
// buffered clipping area for text being drawn
private Area textClippingArea;
private final Map<PDFont, Glyph2D> fontGlyph2D = new HashMap<PDFont, Glyph2D>();
/**
* Constructor.
*
* @param renderer renderer to render the page.
* @param page the page that is to be rendered.
* @throws IOException If there is an error loading properties from the file.
*/
public PageDrawer(PDFRenderer renderer, PDPage page) throws IOException
{
super(page);
this.renderer = renderer;
}
/**
* Returns the parent renderer.
*/
public PDFRenderer getRenderer()
{
return renderer;
}
/**
* Sets high-quality rendering hints on the current Graphics2D.
*/
private void setRenderingHints()
{
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
graphics.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
/**
* Draws the page to the requested context.
*
* @param g The graphics context to draw onto.
* @param pageSize The size of the page to draw.
* @throws IOException If there is an IO error while drawing the page.
*/
public void drawPage(Graphics g, PDRectangle pageSize) throws IOException
{
graphics = (Graphics2D) g;
xform = graphics.getTransform();
this.pageSize = pageSize;
setRenderingHints();
graphics.translate(0, (int) pageSize.getHeight());
graphics.scale(1, -1);
// TODO use getStroke() to set the initial stroke
graphics.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
// adjust for non-(0,0) crop box
graphics.translate(-pageSize.getLowerLeftX(), -pageSize.getLowerLeftY());
processPage(getPage());
for (PDAnnotation annotation : getPage().getAnnotations())
{
showAnnotation(annotation);
}
graphics = null;
}
/**
* Draws the pattern stream to the requested context.
*
* @param g The graphics context to draw onto.
* @param pattern The tiling pattern to be used.
* @param colorSpace color space for this tiling.
* @param color color for this tiling.
* @throws IOException If there is an IO error while drawing the page.
*/
public void drawTilingPattern(Graphics2D g, PDTilingPattern pattern, PDColorSpace colorSpace,
PDColor color) throws IOException
{
Graphics2D oldGraphics = graphics;
graphics = g;
GeneralPath oldLinePath = linePath;
linePath = new GeneralPath();
Area oldLastClip = lastClip;
lastClip = null;
setRenderingHints();
saveGraphicsState();
// non-colored patterns have to be given a color
if (colorSpace != null)
{
color = new PDColor(color.getComponents(), colorSpace);
getGraphicsState().setNonStrokingColorSpace(colorSpace);
getGraphicsState().setNonStrokingColor(color);
getGraphicsState().setStrokingColorSpace(colorSpace);
getGraphicsState().setStrokingColor(color);
}
processTilingPattern(pattern);
restoreGraphicsState();
graphics = oldGraphics;
linePath = oldLinePath;
lastClip = oldLastClip;
}
/**
* Returns an AWT paint for the given PDColor.
*/
private Paint getPaint(PDColor color) throws IOException
{
PDColorSpace colorSpace = color.getColorSpace();
if (!(colorSpace instanceof PDPattern))
{
float[] rgb = colorSpace.toRGB(color.getComponents());
return new Color(rgb[0], rgb[1], rgb[2]);
}
else
{
PDPattern patternSpace = (PDPattern)colorSpace;
PDAbstractPattern pattern = patternSpace.getPattern(color);
if (pattern instanceof PDTilingPattern)
{
PDTilingPattern tilingPattern = (PDTilingPattern) pattern;
if (tilingPattern.getPaintType() == PDTilingPattern.PAINT_COLORED)
{
// colored tiling pattern
return new TilingPaint(this, tilingPattern, xform);
}
else
{
// uncolored tiling pattern
return new TilingPaint(this, tilingPattern,
patternSpace.getUnderlyingColorSpace(), color, xform);
}
}
else
{
PDShadingPattern shadingPattern = (PDShadingPattern)pattern;
PDShading shading = shadingPattern.getShading();
if (shading == null)
{
LOG.error("shadingPattern is null, will be filled with transparency");
return new Color(0,0,0,0);
}
// fixme: shading needs to use the correct matrix
Matrix patternMatrix = shadingPattern.getMatrix();
if (patternMatrix == null)
{
patternMatrix = new Matrix();
}
return shading.toPaint(patternMatrix);
}
}
}
// sets the clipping path using caching for performance, we track lastClip manually because
// Graphics2D#getClip() returns a new object instead of the same one passed to setClip
private void setClip()
{
Area clippingPath = getGraphicsState().getCurrentClippingPath();
if (clippingPath != lastClip)
{
graphics.setClip(clippingPath);
lastClip = clippingPath;
}
}
@Override
public void beginText() throws IOException
{
setClip();
}
@Override
protected void showText(byte[] string) throws IOException
{
PDGraphicsState state = getGraphicsState();
RenderingMode renderingMode = state.getTextState().getRenderingMode();
// buffer the text clip because it represents a single clipping area
if (renderingMode.isClip())
{
textClippingArea = new Area();
}
super.showText(string);
// apply the buffered clip as one area
if (renderingMode.isClip())
{
state.intersectClippingPath(textClippingArea);
textClippingArea = null;
}
}
@Override
protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode,
Vector displacement) throws IOException
{
AffineTransform at = textRenderingMatrix.createAffineTransform();
at.concatenate(font.getFontMatrix().createAffineTransform());
Glyph2D glyph2D = createGlyph2D(font);
drawGlyph2D(glyph2D, font, code, displacement, at);
}
/**
* Render the font using the Glyph2D interface.
*
* @param glyph2D the Glyph2D implementation provided a GeneralPath for each glyph
* @param font the font
* @param code character code
* @param displacement the glyph's displacement (advance)
* @param at the transformation
* @throws IOException if something went wrong
*/
private void drawGlyph2D(Glyph2D glyph2D, PDFont font, int code, Vector displacement,
AffineTransform at) throws IOException
{
PDGraphicsState state = getGraphicsState();
RenderingMode renderingMode = state.getTextState().getRenderingMode();
GeneralPath path = glyph2D.getPathForCharacterCode(code);
if (path != null)
{
// stretch non-embedded glyph if it does not match the width contained in the PDF
if (!font.isEmbedded())
{
float fontWidth = font.getWidthFromFont(code);
if (fontWidth > 0 && // ignore spaces
Math.abs(fontWidth - displacement.getX() * 1000) > 0.0001)
{
float pdfWidth = displacement.getX() * 1000;
at.scale(pdfWidth / fontWidth, 1);
}
}
// render glyph
Shape glyph = at.createTransformedShape(path);
if (renderingMode.isFill())
{
graphics.setComposite(state.getNonStrokingJavaComposite());
graphics.setPaint(getNonStrokingPaint());
setClip();
graphics.fill(glyph);
}
if (renderingMode.isStroke())
{
graphics.setComposite(state.getStrokingJavaComposite());
graphics.setPaint(getStrokingPaint());
graphics.setStroke(getStroke());
setClip();
graphics.draw(glyph);
}
if (renderingMode.isClip())
{
textClippingArea.add(new Area(glyph));
}
}
}
/**
* Provide a Glyph2D for the given font.
*
* @param font the font
* @return the implementation of the Glyph2D interface for the given font
* @throws IOException if something went wrong
*/
private Glyph2D createGlyph2D(PDFont font) throws IOException
{
// Is there already a Glyph2D for the given font?
if (fontGlyph2D.containsKey(font))
{
return fontGlyph2D.get(font);
}
Glyph2D glyph2D = null;
if (font instanceof PDTrueTypeFont)
{
PDTrueTypeFont ttfFont = (PDTrueTypeFont)font;
glyph2D = new TTFGlyph2D(ttfFont); // TTF is never null
}
else if (font instanceof PDType1Font)
{
PDType1Font pdType1Font = (PDType1Font)font;
glyph2D = new Type1Glyph2D(pdType1Font); // T1 is never null
}
else if (font instanceof PDType1CFont)
{
PDType1CFont type1CFont = (PDType1CFont)font;
if (type1CFont.getCFFType1Font() != null) // todo: could be null (need to incorporate fallback)
{
glyph2D = new Type1Glyph2D(type1CFont);
}
}
else if (font instanceof PDType0Font)
{
PDType0Font type0Font = (PDType0Font) font;
if (type0Font.getDescendantFont() instanceof PDCIDFontType2)
{
glyph2D = new TTFGlyph2D(type0Font); // TTF is never null
}
else if (type0Font.getDescendantFont() instanceof PDCIDFontType0)
{
// a Type0 CIDFont contains CFF font
PDCIDFontType0 cidType0Font = (PDCIDFontType0)type0Font.getDescendantFont();
glyph2D = new CIDType0Glyph2D(cidType0Font); // todo: could be null (need incorporate fallback)
}
}
else
{
throw new IllegalStateException("Bad font type: " + font.getClass().getSimpleName());
}
// cache the Glyph2D instance
if (glyph2D != null)
{
fontGlyph2D.put(font, glyph2D);
}
if (glyph2D == null)
{
// todo: make sure this never happens
throw new UnsupportedOperationException("No font for " + font.getName());
}
return glyph2D;
}
@Override
public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3)
{
// to ensure that the path is created in the right direction, we have to create
// it by combining single lines instead of creating a simple rectangle
linePath.moveTo((float) p0.getX(), (float) p0.getY());
linePath.lineTo((float) p1.getX(), (float) p1.getY());
linePath.lineTo((float) p2.getX(), (float) p2.getY());
linePath.lineTo((float) p3.getX(), (float) p3.getY());
// close the subpath instead of adding the last line so that a possible set line
// cap style isn't taken into account at the "beginning" of the rectangle
linePath.closePath();
}
/**
* Generates AWT raster for a soft mask
*
* @param softMask soft mask
* @return AWT raster for soft mask
* @throws IOException
*/
private Raster createSoftMaskRaster(PDSoftMask softMask) throws IOException
{
TransparencyGroup transparencyGroup = new TransparencyGroup(softMask.getGroup());
COSName subtype = softMask.getSubType();
if (COSName.ALPHA.equals(subtype))
{
return transparencyGroup.getAlphaRaster();
}
else if (COSName.LUMINOSITY.equals(subtype))
{
return transparencyGroup.getLuminosityRaster();
}
else
{
throw new IOException("Invalid soft mask subtype.");
}
}
private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throws IOException
{
if (softMask != null)
{
return new SoftMaskPaint(parentPaint, createSoftMaskRaster(softMask));
}
else
{
return parentPaint;
}
}
// returns the stroking AWT Paint
private Paint getStrokingPaint() throws IOException
{
return applySoftMaskToPaint(
getPaint(getGraphicsState().getStrokingColor()),
getGraphicsState().getSoftMask());
}
// returns the non-stroking AWT Paint
private Paint getNonStrokingPaint() throws IOException
{
return getPaint(getGraphicsState().getNonStrokingColor());
}
// create a new stroke based on the current CTM and the current stroke
private BasicStroke getStroke()
{
PDGraphicsState state = getGraphicsState();
// apply the CTM
float lineWidth = transformWidth(state.getLineWidth());
// minimum line width as used by Adobe Reader
if (lineWidth < 0.25)
{
lineWidth = 0.25f;
}
PDLineDashPattern dashPattern = state.getLineDashPattern();
int phaseStart = dashPattern.getPhase();
float[] dashArray = dashPattern.getDashArray();
if (dashArray != null)
{
// apply the CTM
for (int i = 0; i < dashArray.length; ++i)
{
dashArray[i] = transformWidth(dashArray[i]);
}
phaseStart = (int)transformWidth(phaseStart);
// empty dash array is illegal
if (dashArray.length == 0)
{
dashArray = null;
}
}
return new BasicStroke(lineWidth, state.getLineCap(), state.getLineJoin(),
state.getMiterLimit(), dashArray, phaseStart);
}
@Override
public void strokePath() throws IOException
{
graphics.setComposite(getGraphicsState().getStrokingJavaComposite());
graphics.setPaint(getStrokingPaint());
graphics.setStroke(getStroke());
setClip();
graphics.draw(linePath);
linePath.reset();
}
@Override
public void fillPath(int windingRule) throws IOException
{
graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
graphics.setPaint(getNonStrokingPaint());
setClip();
linePath.setWindingRule(windingRule);
// disable anti-aliasing for rectangular paths, this is a workaround to avoid small stripes
// which occur when solid fills are used to simulate piecewise gradients, see PDFBOX-2302
boolean isRectangular = isRectangular(linePath);
if (isRectangular)
{
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
}
graphics.fill(linePath);
linePath.reset();
if (isRectangular)
{
// JDK 1.7 has a bug where rendering hints are reset by the above call to
// the setRenderingHint method, so we re-set all hints, see PDFBOX-2302
setRenderingHints();
}
}
/**
* Returns true if the given path is rectangular.
*/
private boolean isRectangular(GeneralPath path)
{
PathIterator iter = path.getPathIterator(null);
double[] coords = new double[6];
int count = 0;
int[] xs = new int[4];
int[] ys = new int[4];
while (!iter.isDone())
{
switch(iter.currentSegment(coords))
{
case PathIterator.SEG_MOVETO:
if (count == 0)
{
xs[count] = (int)Math.floor(coords[0]);
ys[count] = (int)Math.floor(coords[1]);
}
else
{
return false;
}
count++;
break;
case PathIterator.SEG_LINETO:
if (count < 4)
{
xs[count] = (int)Math.floor(coords[0]);
ys[count] = (int)Math.floor(coords[1]);
}
else
{
return false;
}
count++;
break;
case PathIterator.SEG_CUBICTO:
return false;
case PathIterator.SEG_CLOSE:
break;
}
iter.next();
}
if (count == 4)
{
return xs[0] == xs[1] || xs[0] == xs[2] ||
ys[0] == ys[1] || ys[0] == ys[3];
}
return false;
}
/**
* Fills and then strokes the path.
*
* @param windingRule The winding rule this path will use.
* @throws IOException If there is an IO error while filling the path.
*/
@Override
public void fillAndStrokePath(int windingRule) throws IOException
{
// TODO can we avoid cloning the path?
GeneralPath path = (GeneralPath)linePath.clone();
fillPath(windingRule);
linePath = path;
strokePath();
}
@Override
public void clip(int windingRule)
{
// the clipping path will not be updated until the succeeding painting operator is called
clipWindingRule = windingRule;
}
@Override
public void moveTo(float x, float y)
{
linePath.moveTo(x, y);
}
@Override
public void lineTo(float x, float y)
{
linePath.lineTo(x, y);
}
@Override
public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3)
{
linePath.curveTo(x1, y1, x2, y2, x3, y3);
}
@Override
public Point2D getCurrentPoint()
{
return linePath.getCurrentPoint();
}
@Override
public void closePath()
{
linePath.closePath();
}
@Override
public void endPath()
{
if (clipWindingRule != -1)
{
linePath.setWindingRule(clipWindingRule);
getGraphicsState().intersectClippingPath(linePath);
clipWindingRule = -1;
}
linePath.reset();
}
@Override
public void drawImage(PDImage pdImage) throws IOException
{
Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
AffineTransform at = ctm.createAffineTransform();
if (pdImage.isStencil())
{
// fill the image with paint
PDColor color = getGraphicsState().getNonStrokingColor();
BufferedImage image = pdImage.getStencilImage(getPaint(color));
// draw the image
drawBufferedImage(image, at);
}
else
{
if (!pdImage.getInterpolate())
{
boolean isScaledUp = Math.round(pdImage.getWidth()) < Math.round(at.getScaleX()) ||
Math.round(pdImage.getHeight()) < Math.round(at.getScaleY());
// if the image is scaled down, we use smooth interpolation, eg PDFBOX-2364
// only when scaled up do we use nearest neighbour, eg PDFBOX-2302 / mori-cvpr01.pdf
if (isScaledUp)
{
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
}
}
// draw the image
drawBufferedImage(pdImage.getImage(), at);
if (!pdImage.getInterpolate())
{
// JDK 1.7 has a bug where rendering hints are reset by the above call to
// the setRenderingHint method, so we re-set all hints, see PDFBOX-2302
setRenderingHints();
}
}
}
public void drawBufferedImage(BufferedImage image, AffineTransform at) throws IOException
{
graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
setClip();
PDSoftMask softMask = getGraphicsState().getSoftMask();
if( softMask != null )
{
AffineTransform imageTransform = new AffineTransform(at);
imageTransform.scale(1, -1);
imageTransform.translate(0, -1);
Paint awtPaint = new TexturePaint(image,
new Rectangle2D.Double(imageTransform.getTranslateX(), imageTransform.getTranslateY(),
imageTransform.getScaleX(), imageTransform.getScaleY()));
awtPaint = applySoftMaskToPaint(awtPaint, softMask);
graphics.setPaint(awtPaint);
Rectangle2D unitRect = new Rectangle2D.Float(0, 0, 1, 1);
graphics.fill(at.createTransformedShape(unitRect));
}
else
{
int width = image.getWidth(null);
int height = image.getHeight(null);
AffineTransform imageTransform = new AffineTransform(at);
imageTransform.scale(1.0 / width, -1.0 / height);
imageTransform.translate(0, -height);
graphics.drawImage(image, imageTransform, null);
}
}
@Override
public void shadingFill(COSName shadingName) throws IOException
{
PDShading shading = getResources().getShading(shadingName);
Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
Paint paint = shading.toPaint(ctm);
graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
graphics.setPaint(paint);
graphics.setClip(null);
lastClip = null;
graphics.fill(getGraphicsState().getCurrentClippingPath());
}
@Override
public void showAnnotation(PDAnnotation annotation) throws IOException
{
lastClip = null;
super.showAnnotation(annotation);
}
@Override
public void showTransparencyGroup(PDFormXObject form) throws IOException
{
TransparencyGroup group = new TransparencyGroup(form);
graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
setClip();
// both the DPI xform and the CTM were already applied to the group, so all we do
// here is draw it directly onto the Graphics2D device at the appropriate position
PDRectangle bbox = group.getBBox();
AffineTransform prev = graphics.getTransform();
float x = bbox.getLowerLeftX();
float y = pageSize.getHeight() - bbox.getLowerLeftY() - bbox.getHeight();
graphics.setTransform(AffineTransform.getTranslateInstance(x * xform.getScaleX(),
y * xform.getScaleY()));
PDSoftMask softMask = getGraphicsState().getSoftMask();
if (softMask != null)
{
BufferedImage image = group.getImage();
Paint awtPaint = new TexturePaint(image,
new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight()));
awtPaint = applySoftMaskToPaint(awtPaint, softMask); // todo: PDFBOX-994 problem here?
graphics.setPaint(awtPaint);
graphics.fill(new Rectangle2D.Float(0, 0, bbox.getWidth() * (float)xform.getScaleX(),
bbox.getHeight() * (float)xform.getScaleY()));
}
else
{
graphics.drawImage(group.getImage(), null, null);
}
graphics.setTransform(prev);
}
/**
* Transparency group.
**/
private final class TransparencyGroup
{
private final BufferedImage image;
private final PDRectangle bbox;
private final int minX;
private final int minY;
private final int width;
private final int height;
/**
* Creates a buffered image for a transparency group result.
*/
private TransparencyGroup(PDFormXObject form) throws IOException
{
Graphics2D g2dOriginal = graphics;
Area lastClipOriginal = lastClip;
// get the CTM x Form Matrix transform
Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
Matrix transform = Matrix.concatenate(ctm, form.getMatrix());
// transform the bbox
PDRectangle bbox = form.getBBox().transform(transform);
// clip the bbox to prevent giant bboxes from consuming all memory
Area clip = (Area)getGraphicsState().getCurrentClippingPath().clone();
clip.intersect(new Area(bbox.toGeneralPath()));
Rectangle2D clipRect = clip.getBounds2D();
this.bbox = new PDRectangle((float)clipRect.getX(), (float)clipRect.getY(),
(float)clipRect.getWidth(), (float)clipRect.getHeight());
// apply the underlying Graphics2D device's DPI transform
Shape deviceClip = xform.createTransformedShape(clip);
Rectangle2D bounds = deviceClip.getBounds2D();
minX = (int) Math.floor(bounds.getMinX());
minY = (int) Math.floor(bounds.getMinY());
int maxX = (int) Math.floor(bounds.getMaxX()) + 1;
int maxY = (int) Math.floor(bounds.getMaxY()) + 1;
width = maxX - minX;
height = maxY - minY;
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // FIXME - color space
Graphics2D g = image.createGraphics();
// flip y-axis
g.translate(0, height);
g.scale(1, -1);
// apply device transform (DPI)
g.transform(xform);
// adjust the origin
g.translate(-clipRect.getX(), -clipRect.getY());
graphics = g;
try
{
processTransparencyGroup(form);
}
finally
{
lastClip = lastClipOriginal;
graphics.dispose();
graphics = g2dOriginal;
}
}
public BufferedImage getImage()
{
return image;
}
public PDRectangle getBBox()
{
return bbox;
}
public Raster getAlphaRaster()
{
return image.getAlphaRaster();
}
public Raster getLuminosityRaster()
{
BufferedImage gray = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
Graphics g = gray.getGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return gray.getRaster();
}
}
}