// ViewImage.java // ----------------------- // part of YaCy // (C) by Michael Peter Christen; mc@yacy.net // first published on http://www.anomic.de // Frankfurt, Germany, 2006 // created 03.04.2006 // // 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 2 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, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; import java.awt.MediaTracker; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.Raster; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.Map; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import net.yacy.cora.document.id.DigestURL; import net.yacy.cora.document.id.MultiProtocolURL; import net.yacy.cora.federate.yacy.CacheStrategy; import net.yacy.cora.protocol.ClientIdentification; import net.yacy.cora.protocol.Domains; import net.yacy.cora.protocol.HeaderFramework; import net.yacy.cora.protocol.RequestHeader; import net.yacy.cora.storage.ConcurrentARC; import net.yacy.cora.util.ConcurrentLog; import net.yacy.data.URLLicense; import net.yacy.kelondro.util.FileUtils; import net.yacy.kelondro.util.MemoryControl; import net.yacy.kelondro.workflow.WorkflowProcessor; import net.yacy.peers.graphics.EncodedImage; import net.yacy.repository.Blacklist.BlacklistType; import net.yacy.repository.LoaderDispatcher; import net.yacy.search.Switchboard; import net.yacy.server.serverObjects; import net.yacy.server.serverSwitch; public class ViewImage { private static Map iconcache = new ConcurrentARC(1000, Math.max(10, Math.min(32, WorkflowProcessor.availableCPU * 2))); private static String defaulticon = "htroot/env/grafics/dfltfvcn.ico"; private static byte[] defaulticonb = null; /** * Try parsing image from post "url" parameter or from "code" parameter. * When image format is not supported, return directly image data. When * image could be parsed, try encoding to target format specified by header * "EXT". * * @param header * request header * @param post * post parameters * @param env * environment * @return an {@link EncodedImage} instance encoded in format specified in * post, or an InputStream pointing to original image data. * Return and EncodedImage with empty data when image format is not supported, * a read/write or any other error occured while loading resource. * @throws IOException * when specified url is malformed. * Sould end in a HTTP 500 error whose processing is more * consistent across browsers than a response with zero content * bytes. */ public static Object respond(final RequestHeader header, final serverObjects post, final serverSwitch env) throws IOException { final Switchboard sb = (Switchboard) env; // the url to the image can be either submitted with an url in clear // text, or using a license key // if the url is given as clear text, the user must be authorized as // admin // the license can be used also from non-authorized users String urlString = post.get("url", ""); final String urlLicense = post.get("code", ""); String ext = header.get("EXT", null); final boolean auth = Domains.isLocalhost(header.get(HeaderFramework.CONNECTION_PROP_CLIENTIP, "")) || sb.verifyAuthentication(header); // handle access rights DigestURL url = null; if ((urlString.length() > 0) && (auth)) { url = new DigestURL(urlString); } if ((url == null) && (urlLicense.length() > 0)) { urlString = URLLicense.releaseLicense(urlLicense); if (urlString != null) { url = new DigestURL(urlString); } else { // license is gone (e.g. released/remove in prev calls) ConcurrentLog.fine("ViewImage", "image urlLicense not found key=" + urlLicense); /* Return an empty EncodedImage. Caller is responsible for handling this correctly (500 status code response) */ return new EncodedImage(new byte[0], ext, post.getBoolean("isStatic")); // TODO: maybe favicon accessed again, check // iconcache } } // get the image as stream if (MemoryControl.shortStatus()) { iconcache.clear(); } EncodedImage encodedImage = null; Image image = iconcache.get(urlString); if (image != null) { encodedImage = new EncodedImage(image, ext, post.getBoolean("isStatic")); } else { ImageInputStream imageInStream = null; InputStream inStream = null; try { String urlExt = MultiProtocolURL.getFileExtension(url.getFileName()); if (ext != null && ext.equalsIgnoreCase(urlExt) && isBrowserRendered(urlExt)) { return openInputStream(post, sb.loader, auth, url); } /* * When opening a file, the most efficient is to open * ImageInputStream directly on file */ if (url.isFile()) { imageInStream = ImageIO.createImageInputStream(url.getFSFile()); } else { inStream = openInputStream(post, sb.loader, auth, url); imageInStream = ImageIO.createImageInputStream(inStream); } // read image encodedImage = parseAndScale(post, auth, urlString, ext, imageInStream); } catch(Exception e) { /* Exceptions are not propagated here : many error causes are possible, network errors, * incorrect or unsupported format, bad ImageIO plugin... * Instead return an empty EncodedImage. Caller is responsible for handling this correctly (500 status code response) */ if ("favicon.ico".equalsIgnoreCase(url.getFileName())) { // but on missing favicon just present a default (occures frequently by call from searchitem.html) // currently yacysearchitem assigns "hosturl/favicon.ico" (to look for the filename should not much interfere with other situatios) if (defaulticonb == null) { // load the default icon once try { defaulticonb = FileUtils.read(new File(sb.getAppPath(), defaulticon)); } catch (final IOException initicon) { defaulticonb = new byte[0]; } } encodedImage = new EncodedImage(defaulticonb, ext, post.getBoolean("isStatic")); } else { encodedImage = new EncodedImage(new byte[0], ext, post.getBoolean("isStatic")); } } finally { /* * imageInStream.close() method doesn't close source input * stream */ if (inStream != null) { try { inStream.close(); } catch (IOException ignored) { } } } } return encodedImage; } /** * Open input stream on image url using provided loader. All parameters must * not be null. * * @param post * post parameters. * @param loader. * Resources loader. * @param auth * true when user has credentials to load full images. * @param url * image url. * @return an open input stream instance (don't forget to close it). * @throws IOException * when a read/write error occured. */ private static InputStream openInputStream(final serverObjects post, final LoaderDispatcher loader, final boolean auth, DigestURL url) throws IOException { InputStream inStream = null; if (url != null) { try { String agentName = post.get("agentName", auth ? ClientIdentification.yacyIntranetCrawlerAgentName : ClientIdentification.yacyInternetCrawlerAgentName); ClientIdentification.Agent agent = ClientIdentification.getAgent(agentName); inStream = loader.openInputStream(loader.request(url, false, true), CacheStrategy.IFEXIST, BlacklistType.SEARCH, agent); } catch (final IOException e) { /** No need to log full stack trace (in most cases resource is not available because of a network error) */ ConcurrentLog.fine("ViewImage", "cannot load image. URL : " + url.toNormalform(true)); throw e; } } if (inStream == null) { throw new IOException("Input stream could no be open"); } return inStream; } /** * @param formatName * informal file format name. For example : "png". * @return true when image format is rendered by browser and not by * ViewImage internals */ public static boolean isBrowserRendered(String formatName) { /* * gif images are not loaded because of an animated gif bug within jvm * which sends java into an endless loop with high CPU */ /* * svg images not supported by jdk, but by most browser, deliver just * content (without crop/scale) */ return ("gif".equalsIgnoreCase(formatName) || "svg".equalsIgnoreCase(formatName)); } /** * Process source image to try to produce an EncodedImage instance * eventually scaled and clipped depending on post parameters. When * processed, imageInStream is closed. * * @param post * request post parameters. Must not be null. * @param auth * true when access rigths are OK. * @param urlString * image source URL as String. Must not be null. * @param ext * target image file format. May be null. * @param imageInStream * open stream on image content. Must not be null. * @return an EncodedImage instance. * @throws IOException * when image could not be parsed or encoded to specified format. */ protected static EncodedImage parseAndScale(serverObjects post, boolean auth, String urlString, String ext, ImageInputStream imageInStream) throws IOException { EncodedImage encodedImage; // BufferedImage image = ImageIO.read(imageInStream); Iterator readers = ImageIO.getImageReaders(imageInStream); if (!readers.hasNext()) { try { /* When no reader can be found, we have to close the stream */ imageInStream.close(); } catch (IOException ignoredException) { } String errorMessage = "Image format (" + ext + ") is not supported."; ConcurrentLog.fine("ViewImage", errorMessage + "Image URL : " + urlString); /* * Throw an exception, wich will end in a HTTP 500 response, better * handled by browsers than an empty image */ throw new IOException(errorMessage); } ImageReader reader = readers.next(); reader.setInput(imageInStream, true, true); int maxwidth = post.getInt("maxwidth", 0); int maxheight = post.getInt("maxheight", 0); final boolean quadratic = post.containsKey("quadratic"); boolean isStatic = post.getBoolean("isStatic"); BufferedImage image = null; boolean returnRaw = true; if (!auth || maxwidth != 0 || maxheight != 0) { // find original size final int originWidth = reader.getWidth(0); final int originHeigth = reader.getHeight(0); // in case of not-authorized access shrink the image to // prevent // copyright problems, so that images are not larger than // thumbnails Dimension maxDimensions = calculateMaxDimensions(auth, originWidth, originHeigth, maxwidth, maxheight); // if a quadratic flag is set, we cut the image out to be in // quadratic shape int w = originWidth; int h = originHeigth; if (quadratic && originWidth != originHeigth) { Rectangle square = getMaxSquare(originHeigth, originWidth); h = square.height; w = square.width; } Dimension finalDimensions = calculateDimensions(w, h, maxDimensions); if (originWidth != finalDimensions.width || originHeigth != finalDimensions.height) { returnRaw = false; image = readImage(reader); if (quadratic && originWidth != originHeigth) { image = makeSquare(image); } image = scale(finalDimensions.width, finalDimensions.height, image); } if (finalDimensions.width == 16 && finalDimensions.height == 16) { // this might be a favicon, store image to cache for // faster // re-load later on if (image == null) { returnRaw = false; image = readImage(reader); } iconcache.put(urlString, image); } } /* Image do not need to be scaled or cropped */ if (returnRaw) { if (!reader.getFormatName().equalsIgnoreCase(ext) || imageInStream.getFlushedPosition() != 0) { /* * image parsing and reencoding is only needed when source image * and target formats differ, or when first bytes have been discarded */ returnRaw = false; image = readImage(reader); } } if (returnRaw) { byte[] imageData = readRawImage(imageInStream); encodedImage = new EncodedImage(imageData, ext, isStatic); } else { /* * An error can still occur when transcoding from buffered image to * target ext : in that case EncodedImage.getImage() is empty. */ encodedImage = new EncodedImage(image, ext, isStatic); if (encodedImage.getImage().length() == 0) { String errorMessage = "Image could not be encoded to format : " + ext; ConcurrentLog.fine("ViewImage", errorMessage + ". Image URL : " + urlString); throw new IOException(errorMessage); } } return encodedImage; } /** * Read image using specified reader and close ImageInputStream source. * Input must have bean set before using * {@link ImageReader#setInput(Object)} * * @param reader * image reader. Must not be null. * @return buffered image * @throws IOException * when an error occured */ private static BufferedImage readImage(ImageReader reader) throws IOException { BufferedImage image; try { image = reader.read(0); } finally { reader.dispose(); Object input = reader.getInput(); if (input instanceof ImageInputStream) { try { ((ImageInputStream) input).close(); } catch (IOException ignoredException) { } } } return image; } /** * Read image data without parsing. * * @param inStream * image source. Must not be null. First bytes must not have been marked discarded ({@link ImageInputStream#getFlushedPosition()} must be zero) * @return image data as bytes * @throws IOException * when a read/write error occured. */ private static byte[] readRawImage(ImageInputStream inStream) throws IOException { byte[] buffer = new byte[4096]; int l = 0; ByteArrayOutputStream outStream = new ByteArrayOutputStream(); inStream.seek(0); try { while ((l = inStream.read(buffer)) >= 0) { outStream.write(buffer, 0, l); } return outStream.toByteArray(); } finally { try { inStream.close(); } catch (IOException ignored) { } } } /** * Calculate image dimensions from image original dimensions, max * dimensions, and target dimensions. * * @return dimensions to render image */ protected static Dimension calculateDimensions(final int originWidth, final int originHeight, final Dimension max) { int resultWidth; int resultHeight; if (max.width < originWidth || max.height < originHeight) { // scale image final double hs = (originWidth <= max.width) ? 1.0 : ((double) max.width) / ((double) originWidth); final double vs = (originHeight <= max.height) ? 1.0 : ((double) max.height) / ((double) originHeight); final double scale = Math.min(hs, vs); // if (!auth) scale = Math.min(scale, 0.6); // this is for copyright // purpose if (scale < 1.0) { resultWidth = Math.max(1, (int) (originWidth * scale)); resultHeight = Math.max(1, (int) (originHeight * scale)); } else { resultWidth = Math.max(1, originWidth); resultHeight = Math.max(1, originHeight); } } else { // do not scale resultWidth = originWidth; resultHeight = originHeight; } return new Dimension(resultWidth, resultHeight); } /** * Calculate image maximum dimentions from original and specified maximum * dimensions * * @param auth * true when acces rigths are OK. * @return maximum dimensions to render image */ protected static Dimension calculateMaxDimensions(final boolean auth, final int originWidth, final int originHeight, final int maxWidth, final int maxHeight) { int resultWidth; int resultHeight; // in case of not-authorized access shrink the image to prevent // copyright problems, so that images are not larger than thumbnails if (auth) { resultWidth = (maxWidth == 0) ? originWidth : maxWidth; resultHeight = (maxHeight == 0) ? originHeight : maxHeight; } else if ((originWidth > 16) || (originHeight > 16)) { resultWidth = Math.min(96, originWidth); resultHeight = Math.min(96, originHeight); } else { resultWidth = 16; resultHeight = 16; } return new Dimension(resultWidth, resultHeight); } /** * Scale image to specified dimensions * * @param width * target width * @param height * target height * @param image * image to scale. Must not be null. * @return a scaled image */ protected static BufferedImage scale(final int width, final int height, final BufferedImage image) { // compute scaled image Image scaled = image.getScaledInstance(width, height, Image.SCALE_AREA_AVERAGING); final MediaTracker mediaTracker = new MediaTracker(new Container()); mediaTracker.addImage(scaled, 0); try { mediaTracker.waitForID(0); } catch (final InterruptedException e) { } // make a BufferedImage out of that BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); try { result.createGraphics().drawImage(scaled, 0, 0, width, height, null); // check outcome final Raster raster = result.getData(); int[] pixel = new int[raster.getSampleModel().getNumBands()]; pixel = raster.getPixel(0, 0, pixel); } catch (final Exception e) { /* * Exception may be caused by source image color model : try now to * convert to RGB before scaling */ try { BufferedImage converted = EncodedImage.convertToRGB(image); scaled = converted.getScaledInstance(width, height, Image.SCALE_AREA_AVERAGING); mediaTracker.addImage(scaled, 1); try { mediaTracker.waitForID(1); } catch (final InterruptedException e2) { } result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); result.createGraphics().drawImage(scaled, 0, 0, width, height, null); // check outcome final Raster raster = result.getData(); int[] pixel = new int[result.getSampleModel().getNumBands()]; pixel = raster.getPixel(0, 0, pixel); } catch (Exception e2) { result = image; } ConcurrentLog.fine("ViewImage", "Image could not be scaled"); } return result; } /** * * @param h * image height * @param w * image width * @return max square area fitting inside dimensions */ protected static Rectangle getMaxSquare(final int h, final int w) { Rectangle square; if (w > h) { final int offset = (w - h) / 2; square = new Rectangle(offset, 0, h, h); } else { final int offset = (h - w) / 2; square = new Rectangle(0, offset, w, w); } return square; } /** * Crop image to make a square * * @param image * image to crop * @return */ protected static BufferedImage makeSquare(BufferedImage image) { final int w = image.getWidth(); final int h = image.getHeight(); if (w > h) { final BufferedImage dst = new BufferedImage(h, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g = dst.createGraphics(); final int offset = (w - h) / 2; try { g.drawImage(image, 0, 0, h - 1, h - 1, offset, 0, h + offset, h - 1, null); } finally { g.dispose(); } image = dst; } else { final BufferedImage dst = new BufferedImage(w, w, BufferedImage.TYPE_INT_ARGB); Graphics2D g = dst.createGraphics(); final int offset = (h - w) / 2; try { g.drawImage(image, 0, 0, w - 1, w - 1, 0, offset, w - 1, w + offset, null); } finally { g.dispose(); } image = dst; } return image; } }