/*
* Copyright 2006-2012 ICEsoft Technologies Inc.
*
* Licensed 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.icepdf.core.pobjects.graphics;
import org.icepdf.core.pobjects.Form;
import org.icepdf.core.pobjects.PRectangle;
import org.icepdf.core.pobjects.Page;
import org.icepdf.core.pobjects.graphics.text.PageText;
import org.icepdf.core.util.Defs;
import org.icepdf.core.util.GraphicsRenderingHints;
import org.icepdf.core.views.swing.PageViewComponentImpl;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import javax.swing.text.html.HTMLDocument.HTMLReader.IsindexAction;
/**
* <p>The Shapes class hold all object that are parsed from a Page's content
* streams. These contained object make up a pages graphics stack which can
* be interated through to paint a page's content.<p>
* <p/>
* <p>This class is genearlly only used by the Content parser during content
* parsing. The class also stores points to the images found in the content
* as well as the text that is encoded on a page.</p>
*
* @since 1.0
*/
public class Shapes {
private static final Logger logger =
Logger.getLogger(Shapes.class.toString());
private static int paintDelay = 200;
// allow scaling of large images to improve clarity on screen
private static boolean scaleImages;
static {
// decide if large images will be scaled
scaleImages =
Defs.sysPropertyBoolean("org.icepdf.core.scaleImages",
true);
try {
// Delay between painting calls.
paintDelay =
Defs.intProperty("org.icepdf.core.views.refreshfrequency",
200);
} catch (NumberFormatException e) {
logger.log(Level.FINE, "Error reading buffered scale factor");
}
}
// Graphics stack for a page's content.
protected Vector<Object> shapes = new Vector<Object>(1000);
// Vector of images found a page.
private Vector<Image> images = new Vector<Image>();
// last colour used during the painting process, avoid unnecessary additions
// to the stack.
// private Color lastColor;
// last basic stroke used, avoids unnecessary additions to the stack
// private BasicStroke lastBasicStroke;
// the collection of objects listening for page paint events
private Page parentPage;
// text extraction data structure
private PageText pageText = new PageText();
/**
* Classes used to make the paint process a little easier, when poping object
* off the stack.
*/
class Draw {
}
class Fill {
}
class Clip {
}
class NoClip {
}
public Shapes() {
}
public PageText getPageText(){
return pageText;
}
/**
* Gets the number of shapes on the shapes stack.
*
* @return number of shapes on the stack
*/
public int getShapesCount() {
if (shapes != null) {
return shapes.size();
} else {
return 0;
}
}
public void setPageParent(Page parent) {
parentPage = parent;
}
/**
* Clean up.
*/
public void dispose() {
//System.out.println(" Shapes Images vector size " + images.size());
for (Image image : images) {
image.flush();
}
// one more try to free up some memory
images.clear();
images.trimToSize();
//System.out.println(" Shapes Shapes vector size " + images.size());
if (shapes != null){
for (Object tmp : shapes) {
if (tmp instanceof Image) {
//System.out.println(" -------------> Found images");
Image image = (Image) tmp;
image.flush();
} else if (tmp instanceof TextSprite) {
((TextSprite) tmp).dispose();
}else if (tmp instanceof Shapes) {
((Shapes) tmp).dispose();
} else {
//System.out.println(" -------------> Found other " + shapes.size());
//tmp = null;
}
}
shapes.clear();
shapes.trimToSize();
shapes = null;
}
if (pageText != null){
pageText.dispose();
pageText = null;
}
}
/**
* Add a new object to the Shapes stack.
*
* @param o object to add to the graphics stack.
*/
public void add(Object o) {
// if we have an new image we'll check to see if scaling is enabled
if (o instanceof Image) {
Image image = (Image) o;
int width = image.getWidth(null);
Image scaledImage;
// do image scaling on larger images. This improves the softness
// of some images that contains black and white text.
if (scaleImages) {
double scaleFactor = 1.0;
if (width > 1000 && width < 1500) {
scaleFactor = 0.75;
} else if (width > 1500) {
scaleFactor = 0.5;
}
if (scaleFactor < 1.0) {
scaledImage = image.getScaledInstance(
(int) (width * scaleFactor), -1, Image.SCALE_SMOOTH);
image.flush();
} else {
scaledImage = image;
}
} else {
scaledImage = image;
}
images.add(scaledImage);
shapes.add(scaledImage);
return;
}
// this allows us to capture images from an xObject. We unwrap
// the images from the xobject shapes vector, otherwise we have no
// way to extract them.
if (o instanceof Vector) {
Vector tmp = (Vector) o;
Iterator iterator = tmp.iterator();
Object tmpImage;
while (iterator.hasNext()) {
tmpImage = iterator.next();
if (tmpImage instanceof Image) {
images.addElement((Image)tmpImage);
}
}
}
// copy any shapes fro xForms.
if (o instanceof Shapes){
Shapes tmp = (Shapes) o;
pageText.getPageLines().addAll(tmp.getPageText().getPageLines());
}
shapes.add(o);
}
/**
* Adds a new draw command to the graphics stack. When the paint method encounters this
* object the current geometric shape is drawn.
*/
public void addDrawCommand() {
shapes.add(new Draw());
}
/**
* Adds a new fill command to the graphics stack. When the paint method encouters this
* object the current geometric shape is filled with the current fill colour.
*/
public void addFillCommand() {
shapes.add(new Fill());
}
/**
* Adds a new clip command to the graphics stack. When the paint method
* encounters this object the current geometic shape is used as the new
* clip shape.
*/
public void addClipCommand() {
shapes.add(new Clip());
}
/**
* Adds a new no clip command to the graphics stack.
*/
public void addNoClipCommand() {
shapes.add(new NoClip());
}
/**
* Paint the graphics stack to the graphics context
*
* @param g graphics context to paint to.
*/
public synchronized void paint(Graphics2D g) {
paint(g, null);
}
/**
* Paint the graphics stack to the graphics context
*
* @param g graphics context to paint to.
* @param pagePainter parent page painter
*/
public synchronized void paint(Graphics2D g, PageViewComponentImpl.PagePainter pagePainter) {
// disable clipping, helps with printing issues on windows where the
// clip can sometimes blank a whole page. This should only be used as
// a lost resort. Buffering to an image is another way to avoid the clip
// problem.
boolean disableClipping =
Defs.sysPropertyBoolean("org.icepdf.core.paint.disableClipping",
false);
// disables alpha painting.
boolean disableAlpha =
Defs.sysPropertyBoolean("org.icepdf.core.paint.disableAlpha",
false);
Shape shape = null;
AffineTransform base = new AffineTransform(g.getTransform());
Shape clip = g.getClip();
// int rule = AlphaComposite.SRC_OVER;
// float alpha = 1.0f;
// g.setComposite(AlphaComposite.getInstance(rule, alpha));
Object nextShape;
Area clipArea = new Area();
// empty content streams can have a null clip, very rare corner case.
if (clip != null){
clipArea = new Area(clip);
}
// System.out.println("Shapes vector size " + shapes.size() + " " + currentClip);
// long startTime = System.currentTimeMillis();
long currentTime;
long lastPaintTime = System.currentTimeMillis();
// int paintCount = 0;
Iterator<Object> shapesEnumeration = shapes.iterator();
try {
while (shapesEnumeration.hasNext() ||
(pagePainter != null && pagePainter.isStopPaintingRequested())) {
// if (pagePainter != null && pagePainter.isStopPaintingRequested()){
// break;
// }
nextShape = shapesEnumeration.next();
if (nextShape instanceof TextSprite) {
TextSprite ts = (TextSprite)nextShape;
if (((TextSprite) nextShape).intersects(clipArea)) {
((TextSprite) nextShape).paint(g);
// Send a PaintPage Event to listeners
currentTime = System.currentTimeMillis();
if (currentTime - lastPaintTime > paintDelay) {
// paintCount++;
lastPaintTime = currentTime;
if (parentPage != null){
parentPage.notifyPaintPageListeners();
}
}
}
} else if (nextShape instanceof Shape) {
shape = (Shape) nextShape;
} else if (nextShape instanceof Fill) {
if (clipArea.intersects(shape.getBounds2D())) {
g.fill(shape);
// Send a PaintPage Event to listeners
currentTime = System.currentTimeMillis();
if (currentTime - lastPaintTime > paintDelay) {
// paintCount++;
lastPaintTime = currentTime;
parentPage.notifyPaintPageListeners();
}
}
} else if (nextShape instanceof AffineTransform) {
AffineTransform af = new AffineTransform(base);
af.concatenate((AffineTransform) nextShape);
g.setTransform(af);
// update current clip shape
if (g.getClip() != null)
clipArea = new Area(g.getClip());
} else if (nextShape instanceof AlphaComposite &&
!disableAlpha) {
g.setComposite((AlphaComposite) nextShape);
} else if (nextShape instanceof Paint) {
g.setPaint((Paint) nextShape);
} else if (nextShape instanceof Clip) {
// Capture the current af for the
// page
AffineTransform af = new AffineTransform(g.getTransform());
// Set the transform to the base, which is fact where the page
// lies in the viewport, very dynamic.
g.setTransform(base);
// apply the clip, which is always the initial paper size,
g.setClip(clip);
// apply the af, which places the clip in the correct location
g.setTransform(af);
if (shape != null && !disableClipping) {
// clip outline
// g.setComposite(AlphaComposite.getInstance(rule, 1.0f));
// Color tmp = g.getColor();
// g.setColor(Color.red);
// g.draw(shape);
// g.setColor(tmp);
// g.setComposite(AlphaComposite.getInstance(rule, alpha));
// apply the new clip
g.clip(shape);
}
// update clip
clipArea = new Area(g.getClip());
} else if (nextShape instanceof Draw) {
if (shape.intersects(clipArea.getBounds2D()) ||
(shape.getBounds2D().getWidth() < 1.0 ||
shape.getBounds2D().getHeight() < 1.0)) {
g.draw(shape);
// Send a PaintPage Event to listeners
currentTime = System.currentTimeMillis();
if (currentTime - lastPaintTime > paintDelay) {
// paintCount++;
lastPaintTime = currentTime;
parentPage.notifyPaintPageListeners();
}
}
} else if (nextShape instanceof NoClip) {
AffineTransform af = new AffineTransform(g.getTransform());
g.setTransform(base);
g.setClip(clip);
g.setTransform(af);
clipArea = new Area(g.getClip());
} else if (nextShape instanceof Stroke) {
g.setStroke((Stroke) nextShape);
} else if (nextShape instanceof Image) {
Image tmpImage = (Image) nextShape;
if (clipArea.intersects(0, 0, 1, 1)) {
try {
g.drawImage(tmpImage, 0, 0, 1, 1, null);
}
catch (OutOfMemoryError memErr) {
// If we have a large image and if we're scaling it down,
// then that tends to make a memory spike.
// So, lets try redrawing it with the crappiest interpolation
// setting, which uses the least memory
int width = tmpImage.getWidth(null);
int height = tmpImage.getHeight(null);
if (width >= 600 && height >= 600) {
AffineTransform at = g.getTransform();
int scaleX = (int) at.getScaleX();
int scaleY = (int) at.getScaleX();
if (scaleX < width || scaleY < height) {
RenderingHints renderingHints = g.getRenderingHints();
Object oldInterpolation = renderingHints.get(RenderingHints.KEY_INTERPOLATION);
try {
renderingHints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g.setRenderingHints(renderingHints);
g.drawImage(tmpImage, 0, 0, 1, 1, null);
}
catch (OutOfMemoryError memErr2) {
shapesEnumeration.remove();
logger.log(Level.FINE, "Image too large to draw", memErr);
}
finally {
renderingHints.put(RenderingHints.KEY_INTERPOLATION, oldInterpolation);
g.setRenderingHints(renderingHints);
currentTime = System.currentTimeMillis();
// Send a PaintPage Event to listeners
if (currentTime - lastPaintTime > paintDelay) {
// paintCount++;
lastPaintTime = currentTime;
parentPage.notifyPaintPageListeners();
}
}
}
}
}
}
}
// paint non transparency group xForm objects.
else if (nextShape instanceof Shapes) {
((Shapes) nextShape).setPageParent(parentPage);
((Shapes) nextShape).paint(g);
((Shapes) nextShape).setPageParent(null);
}
// Handle the painting of xForm transparency groups, this block
// attempts to handle isolated painting by painting to an image
// then painting the image using which ever alph rule is on the
// shapes stack.
else if (nextShape instanceof Form) {
//todo move logic into form object.
Form xForm = (Form)nextShape;
Rectangle2D bBox = xForm.getBBox();
int width = (int)bBox.getWidth();
int height = (int)bBox.getHeight();
// corner cases where some bBoxes don't have a dimension.
if (width == 0){
width = 1;
}
if (height == 0){
height = 1;
}
// create the new image to write too.
BufferedImage bi = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
Graphics2D canvas = bi.createGraphics();
// copy over the rendering hints
canvas.setRenderingHints(g.getRenderingHints());
// get shapes and paint them.
Shapes xFormShapes = xForm.getShapes();
if (xFormShapes != null){
xFormShapes.setPageParent(parentPage);
// translate the coordinate system as we'll paint the g
// graphic at the correctly location later.
canvas.translate(-(int)bBox.getX(),-(int)bBox.getY());
canvas.setClip(bBox);
xFormShapes.paint(canvas);
xFormShapes.setPageParent(null);
}
// finally paint the graphic using the current gs.
g.drawImage(bi, null, (int)bBox.getX(), (int)bBox.getY());
} else if (nextShape instanceof Color) {
g.setColor((Color) nextShape);
}
// handle tiled painting
else if (nextShape instanceof TilingPattern) {
TilingPattern tilingPattern = (TilingPattern)nextShape;
tilingPattern.paintPattern(g, parentPage);
}
// else if (Debug.ex){
// Debug.p("Found unhandled Shapes Operand ");
// }
}
// not pretty, but avoid any problems which disposing a page in the middle
// of a paint.
}
catch (NoSuchElementException e) {
// eat any errors.
}
catch (Exception e) {
logger.log(Level.FINE, "Error painting shapes.", e);
}
// System.out.println("Paint Count " + paintCount);
// long stopTime = System.currentTimeMillis();
// long elapsedTime = stopTime - startTime;
// System.out.println("Paint Time: " + elapsedTime );
}
// Dangerous method
// /**
// * Dump of all objects in graphics stack.
// * @return
// */
// public String toString() {
// StringBuffer sb = new StringBuffer();
// for (int i = 0; i < shapes.size(); i++) {
// sb.append(shapes.elementAt(i).toString() + "\n");
// }
// return sb.toString();
// }
/**
* Gets all the images that where found when parsing the pages' content. Each
* element in the Vector represents a seperate image.
*
* @return all images in a page's content, if any.
*/
public Vector getImages() {
return images;
}
public void contract() {
if (shapes != null) {
if (shapes.capacity() - shapes.size() > 200) {
shapes.trimToSize();
}
}
}
/**
* This method does not belong to this library.
*
* It was added, to get access to the elements in
* the pdf document.
*
* Returns all bounding boxes of all images
* on this page.
*
* Because the position and dimension of an image
* is related to the output size of a pdf document,
* this method needs the desired pdf output size.
*
* @param width
* @param height
* @param renderHintType
* @param boundary
* @param userRotation
* @param userZoom
* @param p
* @return
*/
public Set<Rectangle2D.Double> getBoundingBoxesForImages(int width, int height, int renderHintType, final int boundary,
float userRotation, float userZoom, Page p)
{
// we don't want to draw on the image, so byte binary is enough
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
Graphics2D g2 = (Graphics2D)image.getGraphics();
GraphicsRenderingHints grh = GraphicsRenderingHints.getDefault();
g2.setRenderingHints(grh.getRenderingHints(renderHintType));
AffineTransform at = p.getPageTransform(boundary, userRotation, userZoom);
g2.transform(at);
AffineTransform pageTransform = g2.getTransform();
Shape pageClip = g2.getClip();
setPageParent(p);
// get bounding boxes
Set<Rectangle2D.Double> boundingBoxes = calcBoundingBoxesForimage(g2);
setPageParent(null);
g2.setTransform(pageTransform);
g2.setClip(pageClip);
return boundingBoxes;
}
/**
* This method does not belong to this library.
*
* It was added, to get access to the elements in
* the pdf document.
*
* Returns all bounding boxes of all images
* on this page.
*
* @param widht
* @param height
* @return
*/
private Set<Rectangle2D.Double> calcBoundingBoxesForimage(Graphics2D g)
{
Set<Rectangle2D.Double> boundingBoxes = new HashSet<Rectangle2D.Double>();
// in general it is the same method as paint()
// except this one does not draw, but gets position data
boolean disableClipping = Defs.sysPropertyBoolean("org.icepdf.core.paint.disableClipping", false);
AffineTransform base = new AffineTransform(g.getTransform());
Shape clip = g.getClip();
Object nextShape;
Shape shape = null;
Iterator<Object> shapesEnumeration = shapes.iterator();
try
{
while (shapesEnumeration.hasNext())
{
nextShape = shapesEnumeration.next();
if (nextShape instanceof Shape) {
shape = (Shape) nextShape;
} else if (nextShape instanceof AffineTransform) {
AffineTransform af = new AffineTransform(base);
af.concatenate((AffineTransform) nextShape);
g.setTransform(af);
} else if (nextShape instanceof Clip) {
AffineTransform af = new AffineTransform(g.getTransform());
g.setTransform(base);
g.setClip(clip);
g.setTransform(af);
if (shape != null && !disableClipping) {
g.clip(shape);
}
} else if (nextShape instanceof NoClip) {
AffineTransform af = new AffineTransform(g.getTransform());
g.setTransform(base);
g.setClip(clip);
g.setTransform(af);
} else if (nextShape instanceof Image) {
// get bounding boxes
AffineTransform trans = g.getTransform();
double x = trans.getTranslateX();
double y = trans.getTranslateY();
double w = trans.getScaleX();
double h = trans.getScaleY();
Rectangle2D.Double rect2d = new Rectangle2D.Double(x,y,w,h);
boundingBoxes.add(rect2d);
} else if (nextShape instanceof Shapes) {
((Shapes) nextShape).setPageParent(parentPage);
((Shapes) nextShape).calcBoundingBoxesForimage(g);
((Shapes) nextShape).setPageParent(null);
}
}
}
catch (NoSuchElementException e) {
// eat any errors.
}
catch (Exception e) {
logger.log(Level.FINE, "Error by extracting image boundaries.", e);
}
return boundingBoxes;
}
}