package org.cdlib.xtf.saxonExt.image; /* * Copyright (c) 2007, Regents of the University of California * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the University of California nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.io.IOException; import java.util.HashMap; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import org.cdlib.xtf.servletBase.TextServlet; import org.cdlib.xtf.saxonExt.InstructionWithContent; import net.sf.saxon.expr.Expression; import net.sf.saxon.expr.XPathContext; import net.sf.saxon.instruct.Executable; import net.sf.saxon.instruct.TailCall; import net.sf.saxon.om.AttributeCollection; import net.sf.saxon.om.Axis; import net.sf.saxon.om.AxisIterator; import net.sf.saxon.om.Item; import net.sf.saxon.om.NodeInfo; import net.sf.saxon.om.SequenceIterator; import net.sf.saxon.style.ExtensionInstruction; import net.sf.saxon.trans.DynamicError; import net.sf.saxon.trans.XPathException; import net.sf.saxon.type.Type; /** * Implements a Saxon instruction that reads an image from the filesystem, * optionally modifies it in various ways, and outputs it directly via the * current HttpServletResponse. * * @author Martin Haye */ public class OutputElement extends ExtensionInstruction { HashMap<String, Expression> attribs = new HashMap<String, Expression>(); boolean flipY = false; private final static int outColorBase = 32; private final static ImageCache imageCache = new ImageCache(outColorBase); public void prepareAttributes() throws XPathException { // Check the attributes. AttributeCollection inAtts = getAttributeList(); for (int i=0; i<inAtts.getLength(); i++) { String attName = inAtts.getLocalName(i); String attVal = inAtts.getValue(i); if (attName.matches("^(src|xBias|xScale|yBias|yScale)$")) attribs.put(attName, makeAttributeValueTemplate(attVal)); else if (attName.equals("flipY")) { if (attVal.matches("^(1|true|yes)$")) flipY = true; else if (attVal.matches("^(0|false|no)$")) flipY = false; else this.compileError("'flipy' attribute must be 'yes' or 'no'"); } else this.compileError("Unrecogized attribute '" + attName + "'for image:output element"); } } // prepareAttributes() /** * Determine whether this type of element is allowed to contain a template-body */ public boolean mayContainSequenceConstructor() { return true; } public Expression compile(Executable exec) throws XPathException { Expression content = compileSequenceConstructor(exec, iterateAxis(Axis.CHILD), true); return new OutputInstruction(attribs, flipY, content); } private static class OutputInstruction extends InstructionWithContent { private boolean flipY; private float xBias; private float xScale; private float yBias; private float yScale; private int origHeight; private int cropOffX; private int cropOffY; public OutputInstruction(HashMap<String, Expression> attribs, boolean flipY, Expression content) { super("image:output", attribs, content); this.flipY = flipY; } public TailCall processLeavingTail(XPathContext context) throws XPathException { cropOffX = cropOffY = 0; try { // Interesting workaround: using BufferedImage normally results in a Window // being created. However, since we're running in a servlet container, this // isn't generally desirable (and often isn't possible.) So we let AWT know // that it's running in "headless" mode, and this prevents the window from // being created. // System.setProperty("java.awt.headless", "true"); // Get the bias and scale factors, if any. xBias = getFloatAttrib(context, "xBias", 0); xScale = getFloatAttrib(context, "xScale", 1); yBias = getFloatAttrib(context, "yBias", 0); yScale = getFloatAttrib(context, "yScale", 1); // First, load the source image. The ImageCache will automatically take // care of mapping the palette for us. // String src = attribs.get("src").evaluateAsString(context); String srcPath = TextServlet.getCurServlet().getRealPath(src); BufferedImage bi; try { bi = imageCache.find(srcPath); } catch (Exception e) { throw new RuntimeException(e); } // Make a copy of the image so we don't mess up the original. bi = new BufferedImage(bi.getColorModel(), (WritableRaster) bi.getData(), false, null); // Record the original height (needed for flipY mode) origHeight = bi.getHeight(); // Highlight specified areas, if any. if (content != null) { SequenceIterator iter = content.iterate(context); Item item; while ((item = iter.next()) != null) { // The only sub-elements allowed are "highlight" elements if (!(item instanceof NodeInfo) || ((NodeInfo)item).getNodeKind() != Type.ELEMENT || !((NodeInfo)item).getLocalPart().matches("^(yellowBackground|redForeground|crop)$")) { dynamicError("image:output element may only contain 'yellowBackground', " + "'redForeground', or 'crop' sub-elements", "XTFimgH", context); } // Parse the coordinate attributes NodeInfo node = (NodeInfo) item; Rect rect = parseRect(context, node, bi.getWidth(), bi.getHeight()); if (!rect.isEmpty()) { // Highlight the area if (node.getLocalPart().equals("yellowBackground")) makeBackgroundYellow(bi, rect); else if (node.getLocalPart().equals("redForeground")) makeForegroundRed(bi, rect); else { bi = bi.getSubimage(rect.left, rect.top, rect.width(), rect.height()); cropOffX += rect.left; cropOffY += rect.top; } } } } // Finally, output the result. HttpServletResponse res = TextServlet.getCurResponse(); res.setContentType("image/png"); ServletOutputStream out = res.getOutputStream(); ImageIO.write(bi, "PNG", out); } catch (IOException e) { throw new DynamicError(e); } return null; } /** * Get an attribute value and convert to floating point. If not present, * the default value is used instead. * @throws XPathException */ private float getFloatAttrib(XPathContext context, String attName, float defaultVal) throws XPathException { String strVal = attribs.get(attName).evaluateAsString(context); if (strVal == null) return defaultVal; try { return Float.parseFloat(strVal); } catch (NumberFormatException e) { return defaultVal; } } /** * Parse the "left", "top", "right", and "bottom" attributes from a * "highlight" element. * * @param node The element containing the attributes * @param imgHeight The height to use when flipping * @return A rectangle containing the coordinates (flipped if necessary) * @throws DynamicError */ private Rect parseRect(XPathContext context, NodeInfo node, int imgWidth, int imgHeight) throws DynamicError { AxisIterator atts = node.iterateAxis(Axis.ATTRIBUTE); Item att; int left = 0, top = 0, right = 0, bottom = 0; boolean leftFound = false, rightFound = false, topFound = false, bottomFound = false; while ((att = atts.next()) != null) { String name = ((NodeInfo)att).getLocalPart(); String value = ((NodeInfo)att).getStringValue(); if ("left".equals(name)) { left = Integer.parseInt(value); leftFound = true; } else if ("top".equals(name)) { top = Integer.parseInt(value); topFound = true; } else if ("right".equals(name)) { right = Integer.parseInt(value); rightFound = true; } else if ("bottom".equals(name)) { bottom = Integer.parseInt(value); bottomFound = true; } else dynamicError("Unknown attribute '" + name + "' to '" + node.getLocalPart() + "' element", "XTFimgHAU", context); } if (!(leftFound && rightFound && topFound && bottomFound)) dynamicError("'" + node.getLocalPart() + "' element must specify 'left', 'top', 'right', and 'bottom' attributes", "XTFimgHAltrb", context); // Apply bias and scale factors. left = (int) ((left + xBias) * xScale); top = (int) ((top + yBias) * yScale); right = (int) ((right + xBias) * xScale); bottom = (int) ((bottom + yBias) * yScale); // Flip the Y values if requested if (flipY) { top = origHeight - top; bottom = origHeight - bottom; } // If cropping has been done, adjust the coordinates. left -= cropOffX; top -= cropOffY; right -= cropOffX; bottom -= cropOffY; // Clamp the values to be completely within the image. left = Math.max(Math.min(left, imgWidth), 0); top = Math.max(Math.min(top, imgHeight), 0); right = Math.max(Math.min(right, imgWidth), 0); bottom = Math.max(Math.min(bottom, imgHeight), 0); // Reverse values that are backwards. int tmp; if (left > right) { tmp = left; left = right; right = tmp; } if (top > bottom) { tmp = top; top = bottom; bottom = tmp; } // All done. return new Rect(left, top, right, bottom); } /** * Change white background to yellow in the given area of an image */ private void makeBackgroundYellow(BufferedImage bi, Rect rect) { int w = rect.width(); int h = rect.height(); byte[] data = new byte[w*h]; bi.getRaster().getDataElements(rect.left, rect.top, rect.width(), rect.height(), data); for (int i=0; i<w*h; i++) data[i] |= (outColorBase * 1); bi.getRaster().setDataElements(rect.left, rect.top, rect.width(), rect.height(), data); } /** * Change black foreground to red in the given area of an image */ private void makeForegroundRed(BufferedImage bi, Rect rect) { int w = rect.width(); int h = rect.height(); byte[] data = new byte[w*h]; bi.getRaster().getDataElements(rect.left, rect.top, rect.width(), rect.height(), data); for (int i=0; i<w*h; i++) data[i] |= (outColorBase * 2); bi.getRaster().setDataElements(rect.left, rect.top, rect.width(), rect.height(), data); } } /** A rectangle on the image */ private static class Rect { public Rect(int l, int t, int r, int b) { left = l; top = t; right = r; bottom = b; } public boolean isEmpty() { return (width() <= 0 || height() <= 0); } public int left; public int top; public int right; public int bottom; public int width() { return right - left; } public int height() { return bottom - top; } } } // class OutputElement