papertoolkit.render.SheetRenderer.java Source code

Java tutorial

Introduction

Here is the source code for papertoolkit.render.SheetRenderer.java

Source

package papertoolkit.render;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.List;

import javax.media.jai.TiledImage;

import org.jibble.epsgraphics.EpsGraphics2D;

import papertoolkit.paper.Region;
import papertoolkit.paper.Sheet;
import papertoolkit.pattern.TiledPattern;
import papertoolkit.pattern.TiledPatternGenerator;
import papertoolkit.pattern.coordinates.PatternToSheetMapping;
import papertoolkit.pattern.coordinates.conversion.TiledPatternCoordinateConverter;
import papertoolkit.pattern.output.PDFPatternGenerator;
import papertoolkit.pattern.output.PostscriptPatternGenerator;
import papertoolkit.units.Pixels;
import papertoolkit.units.Points;
import papertoolkit.units.Units;
import papertoolkit.units.coordinates.Coordinates;
import papertoolkit.util.DebugUtils;
import papertoolkit.util.MathUtils;
import papertoolkit.util.files.FileUtils;
import papertoolkit.util.graphics.GraphicsUtils;
import papertoolkit.util.graphics.ImageUtils;
import papertoolkit.util.graphics.JAIUtils;

import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfWriter;

/**
 * <p>
 * This class will render a Sheet into a JPEG, PDF, or Java2D graphics context.
 * </p>
 * <p>
 * For individual regions, it will use specific region renderers (e.g., ImageRenderer, PolygonRenderer, and
 * TextRenderer).
 * </p>
 * <p>
 * <span class="BSDLicense"> This software is distributed under the <a
 * href="http://hci.stanford.edu/research/copyright.txt">BSD License</a>. </span>
 * </p>
 * 
 * @author <a href="http://graphics.stanford.edu/~ronyeh">Ron B Yeh</a> (ronyeh(AT)cs.stanford.edu)
 */
public class SheetRenderer {

    /**
     * Generates pattern for this sheet.
     */
    private TiledPatternGenerator generator;

    /**
     * Allows us to save the pattern info to the same directory as the most recently rendered pdf or ps.
     */
    private File mostRecentlyRenderedFile;

    /**
     * What color should we render the pattern in?
     */
    private Color patternColor = Color.BLACK;

    /**
     * You can make the pattern bigger or smaller depending on your printer... 0 == default. - --> smaller, +
     * --> bigger. Each unit corresponds to two font points.
     */
    private int patternDotSizeAdjustment = 0;

    /**
     * Populate this only when we render the pattern (renderToPDF). After we render to pdf, we can save the
     * information to a file, for so that we can run the application in the future without rendering more
     * pattern.
     */
    private PatternToSheetMapping patternInformation;

    /**
     * By default, any active regions will be overlaid with pattern (unique to at least this sheet, unless
     * otherwise specified).
     */
    protected boolean renderActiveRegionsWithPattern = true;

    /**
     * The sheet we are to render.
     */
    protected Sheet sheet;

    /**
     * Create a new TiledPatternGenerator for this Sheet.
     * 
     * @param s
     */
    public SheetRenderer(Sheet s) {
        this(s, new TiledPatternGenerator());
    }

    /**
     * Feel free to share TiledPatternGenerator between Sheets. That way, you can get unique pattern across
     * multiple Sheets.
     * 
     * @param s
     * @param patternGenerator
     */
    public SheetRenderer(Sheet s, TiledPatternGenerator patternGenerator) {
        sheet = s;
        patternInformation = sheet.getPatternToSheetMapping();
        generator = patternGenerator;
    }

    /**
     * @return
     */
    public PatternToSheetMapping getPatternInformation() {
        return patternInformation;
    }

    /**
     * We will render pattern when outputting PDFs. Rendering pattern to screen is a waste of time, since dots
     * are not resolvable on screen. Perhaps for screen display (i.e., anything < 600 dpi), we should render
     * pattern as a faint dotted overlay?
     * 
     * WARNING: Does not work for multiple sheets, as we will get the same pattern....
     * 
     * @param cb
     *            a content layer returned by iText
     * 
     */
    private void renderPatternToPDF(PdfContentByte cb) {
        // for each region, overlay pattern if it is an active region
        final List<Region> regions = sheet.getRegions();

        // this object will generate the right PDF (itext) calls to create pattern
        final PDFPatternGenerator pgen = new PDFPatternGenerator(cb, sheet.getWidth(), sheet.getHeight());
        pgen.setPatternColor(patternColor);

        // adjust the font size of the pattern...
        pgen.adjustPatternSize(patternDotSizeAdjustment);

        // render each region that is active
        for (Region r : regions) {
            if (!r.isActive()) {
                continue;
            }

            // DebugUtils.println("-------------");

            // add the region's offset from the top left corner of the sheet
            Coordinates regionOffset = sheet.getRegionOffset(r);

            // System.out.println("SheetRenderer: Rendering Pattern:" + r.getShape());
            // DebugUtils.println(r.getOriginX() + " " + r.getOriginY());

            // Figure out the real width and height....
            final Units scaledWidth = r.getWidth();
            final Units scaledHeight = r.getHeight();

            // get pattern of the given width and height
            // by default, the pattern returned will be unique if possible (and a warning thrown
            // otherwise). If you want to use the same pattern in different places, you will
            // need to keep the returned pattern object around
            final TiledPattern pattern = generator.getPattern(scaledWidth, scaledHeight);

            // DebugUtils.println("Rendering Pattern for " + r.getName());
            // render the pattern starting at the region's origin
            pgen.renderPattern(pattern, // the tiled pattern
                    Units.add(r.getOriginX(), regionOffset.getX()), // origin + offset
                    Units.add(r.getOriginY(), regionOffset.getY()));// same, for y

            // also, at this point, we know what pattern we have assigned to each region
            // we should be able to assign a tile configuration to each region
            // We retrieve it from the HashMap so we can SET the values in the line below
            final TiledPatternCoordinateConverter tiledPatternInRegion = (TiledPatternCoordinateConverter) patternInformation
                    .getPatternBoundsOfRegion(r);
            tiledPatternInRegion.setPatternInformationByReadingItFrom(pattern);
            // the name should already be correct, barring the UnitializedMapping business...
            // tiledPatternInRegion.setRegionName(r.getName());
            // now, this object is modified
            // since it is already mapped to the correct region r, we do not need
            // to do anything else!
        }

        // /////////////////////////////////////////////////////
        // /////////////////////////////////////////////////////
        // FOR NOW, SPECIAL CASE THE COMPOUND REGIONS
        // IN THE FUTURE, FIGURE OUT HOW TO INTEGRATE IT NICELY
        // /////////////////////////////////////////////////////
        // /////////////////////////////////////////////////////
        // for (Region r : regions) {
        // if (r instanceof CompoundRegion) {
        // DebugUtils.println("Rendering Pattern for Compound Region!");
        // }
        // }
        // 
        // MUST REARCHITECT Pattern Rendering & Event Handling if we are to allow Compound Regions
        // This is for a future R3 version =\
    }

    /**
     * @param file
     */
    private String renderPatternToPostScript() {
        Units width = sheet.getWidth();
        Units height = sheet.getHeight();

        final PostscriptPatternGenerator pgen = new PostscriptPatternGenerator(width, height, generator);

        // for each region, figure out where it is on the sheet, and calculate the pattern coordinates
        final List<Region> regions = sheet.getRegions();
        for (Region r : regions) {
            // We retrieve it from the HashMap so we can SET the values in the line below
            final TiledPatternCoordinateConverter patternCoordinateConverter = (TiledPatternCoordinateConverter) patternInformation
                    .getPatternBoundsOfRegion(r);

            Coordinates regionLocation = sheet.getRegionLocationRelativeToSheet(r);

            if (patternCoordinateConverter == null) {
                DebugUtils.println("Null Converter. Is this region not active? " + r);
                continue;
            }

            // the tiledPattern encompasses the whole sheet
            // we set the information of for the converter, by setting the information for the whole pattern,
            // with a clipping bounds, defined by the region's location relative to
            // the sheet's upper left corner
            patternCoordinateConverter.setPatternInformationByReadingItFrom(pgen.getPattern(), regionLocation,
                    r.getWidth(), r.getHeight());
        }

        return pgen.getPostscriptPattern();
    }

    /**
     * We assume the g2d is big enough for us to draw this Sheet to.
     * 
     * By default, the transforms works at 72 dots per inch. Scale the transform beforehand if you would like
     * better or worse rendering.
     * 
     * This only renders the regions and region content, but not the pattern.
     * 
     * @param g2d
     */
    public void renderToG2D(Graphics2D g2d) {
        // anti-aliased, high quality rendering
        g2d.setRenderingHints(GraphicsUtils.getBestRenderingHints());

        final List<Region> regions = sheet.getRegions();

        // render each region
        for (Region r : regions) {
            // Weird. g2d.getTransform SHOULD give us a copy....
            // a real copy
            final AffineTransform currTransform = new AffineTransform(g2d.getTransform());
            // DebugUtils.println("Rendering " + r.getName());
            final Coordinates regionOffset = sheet.getRegionOffset(r);
            final double xOffsetPts = regionOffset.getX().getValueInPoints();
            final double yOffsetPts = regionOffset.getY().getValueInPoints();
            // System.out.println(xOffsetPts);
            // g2d.transform(AffineTransform.getTranslateInstance(xOffsetPts, yOffsetPts));
            g2d.translate((int) xOffsetPts, (int) yOffsetPts);
            r.getRenderer().renderToG2D(g2d);
            g2d.setTransform(currTransform);
        }
    }

    /**
     * Use the default pixels per inch. Specified in our configuration file.
     * 
     * @param file
     */
    public void renderToJPEG(File file) {
        renderToJPEG(file, Pixels.ONE);
    }

    /**
     * @param destJPEGFile
     * @param destUnits
     *            Converts the graphics2D object into a new coordinate space based on the destination units'
     *            pixels per inch. This is for the purposes of rendering the document to screen, where
     *            Graphics2D's default 72ppi isn't always the right way to do it.
     */
    public void renderToJPEG(File destJPEGFile, Pixels destUnits) {
        final Units width = sheet.getWidth();
        final Units height = sheet.getHeight();

        final double scale = Points.ONE.getScalarMultipleToConvertTo(destUnits);

        final int w = MathUtils.rint(width.getValueIn(destUnits));
        final int h = MathUtils.rint(height.getValueIn(destUnits));
        final TiledImage image = JAIUtils.createWritableBufferWithoutAlpha(w, h);
        final Graphics2D graphics2D = image.createGraphics();
        graphics2D.setRenderingHints(GraphicsUtils.getBestRenderingHints());

        // transform the graphics such that we are in destUnits' pixels per inch, so that when we
        // draw 72 Graphics2D pixels from now on, it will equal the correct number of output pixels
        // in the JPEG.
        graphics2D.setTransform(AffineTransform.getScaleInstance(scale, scale));

        // render a white canvas
        graphics2D.setColor(Color.WHITE);
        graphics2D.fillRect(0, 0, w, h);

        renderToG2D(graphics2D);
        graphics2D.dispose();
        ImageUtils.writeImageToJPEG(image.getAsBufferedImage(), destJPEGFile);
    }

    /**
     * Uses the iText package to render a PDF file from scratch. iText is nice because we can write to a
     * Graphics2D context. Alternatively, we can use PDF-like commands.
     * 
     * @param destPDFFile
     */
    public void renderToPDF(File destPDFFile) {
        try {
            final FileOutputStream fileOutputStream = new FileOutputStream(destPDFFile);

            final Rectangle pageSize = new Rectangle(0, 0, (int) Math.round(sheet.getWidth().getValueInPoints()),
                    (int) Math.round(sheet.getHeight().getValueInPoints()));

            // create a document with these margins (worry about margins later)
            final Document doc = new Document(pageSize, 0, 0, 0, 0);
            final PdfWriter writer = PdfWriter.getInstance(doc, fileOutputStream);
            doc.open();

            final PdfContentByte topLayer = writer.getDirectContent();
            final PdfContentByte bottomLayer = writer.getDirectContentUnder();
            renderToPDFContentLayers(destPDFFile, topLayer, bottomLayer);

            doc.close();

            // save the pattern info to the same directory automatically
            savePatternInformation(); // do this automatically
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    /**
     * @param destPDFFile
     * @param topLayer
     * @param bottomLayer
     */
    protected void renderToPDFContentLayers(File destPDFFile, PdfContentByte topLayer, PdfContentByte bottomLayer) {
        mostRecentlyRenderedFile = destPDFFile;

        final Units width = sheet.getWidth();
        final Units height = sheet.getHeight();
        final float wPoints = (float) width.getValueInPoints();
        final float hPoints = (float) height.getValueInPoints();

        // top layer for regions (changed from bottom layer)
        final Graphics2D g2dOver = topLayer.createGraphicsShapes(wPoints, hPoints);
        // now that we have a G2D, we can just use our other G2D rendering method
        renderToG2D(g2dOver);

        // an efficient dispose, because we are not within a Java paint() method
        g2dOver.dispose();

        // should this be moved to regions???
        if (renderActiveRegionsWithPattern) {
            // DebugUtils.println("Rendering Pattern");
            // after rendering everything, we still need to overlay the pattern on top of active
            // regions; This is only for PDF rendering.

            // top layer for pattern
            renderPatternToPDF(topLayer);
        }
    }

    /**
     * Uses Jibble and some EPS Hacking to create a PostScript file!
     * 
     * @param file
     */
    public void renderToPostScript(File file) {
        mostRecentlyRenderedFile = file;

        // layer for regions
        final EpsGraphics2D g2d = new EpsGraphics2D("PostScript Render");

        // set the bounding box, and draw it, so that the PS file will be the right size\
        g2d.setStroke(new BasicStroke(0.1f));
        g2d.drawRect(0, 0, (int) sheet.getWidth().getValueInPoints(), (int) sheet.getHeight().getValueInPoints());

        // now that we have a G2D, we can just use our other G2D rendering method
        renderToG2D(g2d);
        String graphicsPostscript = g2d.toString();

        // create an associated pattern file
        if (renderActiveRegionsWithPattern) {
            String patternPostscript = renderPatternToPostScript();

            // then, merge the two!

            // remove the junk from graphicsPostscript
            // remove the opening junk
            graphicsPostscript = graphicsPostscript.replaceAll("(?s)%.*EndComments", "");
            // remove the closing junk, including the showpage
            graphicsPostscript = graphicsPostscript.replaceAll("(?s)showpage.*EOF", "");
            // DebugUtils.println(graphicsPostscript);

            // paste these graphics into our pattern
            String output = patternPostscript.replace("__INSERT_SHEET_POSTSCRIPT_HERE__", graphicsPostscript);
            FileUtils.writeStringToFile(output, file);
        } else {
            // just write the graphics to a file
            FileUtils.writeStringToFile(graphicsPostscript, file);
        }

        // save the pattern info to the same directory automatically
        savePatternInformation(); // do this automatically
    }

    /**
     * This saves an xml file with the same name/path, but different extension as the most-recently rendered
     * PDF file.
     */
    public void savePatternInformation() {
        if (mostRecentlyRenderedFile == null) {
            System.err.println("SheetRenderer: We cannot save the pattern information "
                    + "without a destination file. Please render a PDF or PS first "
                    + "so we know where to put the pattern configuration file!");
        } else {
            File parentDir = mostRecentlyRenderedFile.getParentFile();
            String fileName = mostRecentlyRenderedFile.getName();
            if (fileName.contains(".pdf")) {
                fileName = fileName.replace(".pdf", ".patternInfo.xml");
            } else if (fileName.contains(".ps")) {
                fileName = fileName.replace(".ps", ".patternInfo.xml");
            } else {
                fileName = fileName + ".patternInfo.xml";
            }
            File destFile = new File(parentDir, fileName);
            savePatternInformation(destFile);
        }
    }

    /**
     * After Rendering Pattern, we now know the particulars of the pattern coordinates for each region. Save
     * that information to disk.
     * 
     * @param patternInfoFile
     */
    public void savePatternInformation(File patternInfoFile) {
        // save the pattern info to disk as a nice XML File! =)
        patternInformation.saveConfigurationToXML(patternInfoFile);
        // DebugUtils.println("Pattern Information saved to " + patternInfoFile.getAbsolutePath());
    }

    /**
     * This is really just for debugging, as you want BLACK pattern in general.
     * 
     * @param pColor
     */
    public void setPatternColor(Color pColor) {
        patternColor = pColor;
    }

    /**
     * Useful for when rendering many sheets at a time. This can guarantee that the pattern is unique across
     * sheets. If you want to reset the pattern, or pick a particular sheet, you may, by interacting with the
     * generator object.
     * 
     * @param tiledPatternGenerator
     */
    public void setPatternGenerator(TiledPatternGenerator tiledPatternGenerator) {
        generator = tiledPatternGenerator;
    }

    /**
     * Used for debugging. If you set this to false, then we will not render any pattern at all.
     * 
     * @param activeWithPattern
     */
    public void setRenderActiveRegionsWithPattern(boolean activeWithPattern) {
        renderActiveRegionsWithPattern = activeWithPattern;
    }

    /**
     * Call this one or more times before rendering. It's a hint to the renderer.
     */
    public void useLargerPatternDots() {
        patternDotSizeAdjustment++;
    }

    /**
     * Call this one or more times before rendering. It's a hint to the renderer.
     */
    public void useSmallerPatternDots() {
        patternDotSizeAdjustment--;
    }
}