/* * File : $Source: /alkacon/cvs/alkacon/com.alkacon.opencms.v8.weboptimization/src/com/alkacon/opencms/v8/weboptimization/CmsOptimizationSprite.java,v $ * Date : $Date: 2010/01/08 09:46:05 $ * Version: $Revision: 1.3 $ * * This file is part of the Alkacon OpenCms Add-On Module Package * * Copyright (c) 2007 Alkacon Software GmbH (http://www.alkacon.com) * * The Alkacon OpenCms Add-On Module Package 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 3 of the License, or * (at your option) any later version. * * The Alkacon OpenCms Add-On Module Package 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 the Alkacon OpenCms Add-On Module Package. * If not, see http://www.gnu.org/licenses/. * * For further information about Alkacon Software GmbH, please see the * company website: http://www.alkacon.com. * * For further information about OpenCms, please see the * project website: http://www.opencms.org. */ package com.alkacon.opencms.v8.weboptimization; import com.alkacon.simapi.Simapi; import org.opencms.file.CmsFile; import org.opencms.file.CmsObject; import org.opencms.file.CmsResource; import org.opencms.main.CmsIllegalArgumentException; import org.opencms.main.CmsLog; import org.opencms.xml.CmsXmlUtils; import org.opencms.xml.content.CmsXmlContent; import org.opencms.xml.content.CmsXmlContentFactory; import org.opencms.xml.types.I_CmsXmlContentValue; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Iterator; import java.util.Locale; import javax.imageio.ImageIO; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.jsp.PageContext; import org.apache.commons.logging.Log; /** * Bean for optimizing images.<p> * * @author Michael Moossen * * @version $Revision: 1.3 $ * * @since 7.0.6 */ public class CmsOptimizationSprite extends CmsOptimizationBean { /** Node name constant. */ protected static final String N_POSITION = "Position"; /** Node name constant. */ protected static final String N_SELECTOR = "Selector"; /** Node name constant. */ protected static final String N_X = "X"; /** Node name constant. */ protected static final String N_Y = "Y"; /** Image sprite resource type constant. */ protected static final int RESOURCE_TYPE_SPRITE = 764; /** The log object for this class. */ private static final Log LOG = CmsLog.getLog(CmsOptimizationSprite.class); /** * Default constructor. * * @param context the JSP page context object * @param req the JSP request * @param res the JSP response */ public CmsOptimizationSprite(PageContext context, HttpServletRequest req, HttpServletResponse res) { super(context, req, res); } /** * Add a new image to the given sprite.<p> * * @param sprite the sprite to modify * @param image the image to add * @param options the sprite options * * @return the modified sprite */ public BufferedImage addImage(BufferedImage sprite, BufferedImage image, CmsOptimizationSpriteOptions options) { // check the sprite size int newWidth = -1; if (sprite.getWidth() < options.getX() + image.getWidth()) { newWidth = options.getX() + image.getWidth(); } int newHeight = -1; if (sprite.getHeight() < options.getY() + image.getHeight()) { newHeight = options.getY() + image.getHeight(); } Graphics2D g2d = sprite.createGraphics(); if ((newHeight > 0) || (newWidth > 0)) { // adjust sprite size if needed if (newHeight < 0) { newHeight = sprite.getHeight(); } if (newWidth < 0) { newWidth = sprite.getWidth(); } // clone the sprite BufferedImage s2 = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_4BYTE_ABGR); g2d = s2.createGraphics(); g2d.drawImage(sprite, 0, 0, null); sprite = s2; } // put the image into the sprite g2d.drawImage(image, options.getX(), options.getY(), null); return sprite; } /** * @see com.alkacon.opencms.v8.weboptimization.CmsOptimizationBean#includeDefault(java.lang.String, * com.alkacon.opencms.v8.weboptimization.CmsOptimizationBean.IncludeMode) */ @Override public void includeDefault(String path, IncludeMode mode) throws Exception { CmsObject cms = getCmsObject(); // check the resource type CmsFile file = cms.readFile(path); if (file.getTypeId() != RESOURCE_TYPE_SPRITE) { throw new CmsIllegalArgumentException(Messages.get().container( Messages.ERR_NOT_SUPPORTED_RESOURCE_TYPE_2, path, new Integer(file.getTypeId()))); } resolveSpriteInclude(cms, file, mode, null); } /** * Will optimize the resources taken from the underlying XML content.<p> * * @throws Exception if something goes wrong */ public void optimize() throws Exception { CmsObject cms = getCmsObject(); CmsFile file = cms.readFile(cms.getRequestContext().getUri()); // check the resource type String type = Simapi.getImageType(file.getRootPath()); if ((file.getTypeId() != RESOURCE_TYPE_SPRITE) || (type == null)) { throw new CmsIllegalArgumentException(Messages.get().container( Messages.ERR_NOT_SUPPORTED_RESOURCE_TYPE_2, cms.getRequestContext().getUri(), new Integer(file.getTypeId()))); } // read the XML content CmsXmlContent xml = CmsXmlContentFactory.unmarshal(cms, file); // resolve the locale Locale locale = resolveLocale(cms, xml); // cache the current project boolean online = cms.getRequestContext().currentProject().isOnlineProject(); BufferedImage sprite = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR); // iterate the resources Iterator<I_CmsXmlContentValue> itPath = xml.getValues(N_RESOURCE, locale).iterator(); while (itPath.hasNext()) { I_CmsXmlContentValue value = itPath.next(); if (!isOptimized(cms, xml, value, locale, online)) { continue; } // get the path String xpath = CmsXmlUtils.concatXpath(value.getPath(), N_PATH); String path = xml.getValue(xpath, locale).getStringValue(cms); // get the options CmsOptimizationSpriteOptions opts = new CmsOptimizationSpriteOptions(); xpath = CmsXmlUtils.concatXpath(value.getPath(), N_POSITION); if (xml.hasValue(xpath, locale)) { I_CmsXmlContentValue value2 = xml.getValue(xpath, locale); xpath = CmsXmlUtils.concatXpath(value2.getPath(), N_X); opts.setX(Integer.parseInt(xml.getStringValue(cms, xpath, locale))); xpath = CmsXmlUtils.concatXpath(value2.getPath(), N_Y); opts.setY(Integer.parseInt(xml.getStringValue(cms, xpath, locale))); } xpath = CmsXmlUtils.concatXpath(value.getPath(), N_SELECTOR); opts.setSelector(xml.getStringValue(cms, xpath, locale)); // retrieve the actual files to process CmsResource res = cms.readResource(path); if (res.isFolder() || (Simapi.getImageType(path) == null)) { LOG.warn(Messages.get().getBundle().key(Messages.LOG_WARN_NOTHING_TO_PROCESS_1, path)); continue; } // process this resource BufferedImage img = Simapi.read(getBinaryContent(cms, res)); sprite = addImage(sprite, img, opts); } writeImage(sprite, type); } /** * Will create css rules for the given optimized sprite.<p> * * @param path the optimized file uri * @param mode the inclusion mode * @param offset optional position offset, used when called recursively * * @throws Exception if something goes wrong */ protected void resolveSpriteInclude(CmsObject cms, CmsFile file, IncludeMode mode, CmsOptimizationSpriteOptions offset) throws Exception { String path = cms.getSitePath(file); // read the XML content CmsXmlContent xml = CmsXmlContentFactory.unmarshal(cms, file); // resolve the locale Locale locale = resolveLocale(cms, xml); // cache the current project boolean online = cms.getRequestContext().currentProject().isOnlineProject(); // iterate the resources Iterator<I_CmsXmlContentValue> itRes = xml.getValues(N_RESOURCE, locale).iterator(); while (itRes.hasNext()) { I_CmsXmlContentValue value = itRes.next(); // get the uri String xpath = CmsXmlUtils.concatXpath(value.getPath(), N_PATH); String uri = xml.getValue(xpath, locale).getStringValue(cms); // retrieve the actual files to process CmsResource res = cms.readResource(uri); if (res.isFolder() || (Simapi.getImageType(uri) == null)) { LOG.warn(Messages.get().getBundle().key(Messages.LOG_WARN_NOTHING_TO_PROCESS_1, uri)); continue; } boolean optimized = (mode == IncludeMode.OPTIMIZED); if (mode == IncludeMode.AUTO) { // compute the mode to use optimized = isOptimized(cms, xml, value, locale, online); } // get the options CmsOptimizationSpriteOptions opts = new CmsOptimizationSpriteOptions(); xpath = CmsXmlUtils.concatXpath(value.getPath(), N_POSITION); if (xml.hasValue(xpath, locale)) { I_CmsXmlContentValue value2 = xml.getValue(xpath, locale); xpath = CmsXmlUtils.concatXpath(value2.getPath(), N_X); opts.setX(Integer.parseInt(xml.getStringValue(cms, xpath, locale))); xpath = CmsXmlUtils.concatXpath(value2.getPath(), N_Y); opts.setY(Integer.parseInt(xml.getStringValue(cms, xpath, locale))); } xpath = CmsXmlUtils.concatXpath(value.getPath(), N_SELECTOR); opts.setSelector(xml.getStringValue(cms, xpath, locale)); // apply offset if (offset != null) { opts.setX(opts.getX() + offset.getX()); opts.setY(opts.getY() + offset.getY()); } if (res.getTypeId() == RESOURCE_TYPE_SPRITE) { // recurse in case of nested sprites resolveSpriteInclude(cms, cms.readFile(res), mode, opts); } else { // handle this resource if (optimized) { writeSpriteInclude(path, opts); } else { opts.setX(0); opts.setY(0); writeSpriteInclude(uri, opts); } } } } /** * Writes the given image as of the given type to the servlet output stream.<p> * * @param image the image to write * @param type the type * * @throws IOException if something goes wrong */ protected void writeImage(BufferedImage image, String type) throws IOException { ImageWriter writer = (ImageWriter)ImageIO.getImageWritersByFormatName(type).next(); ImageOutputStream stream = ImageIO.createImageOutputStream(getJspContext().getResponse().getOutputStream()); writer.setOutput(stream); writer.write(image); // We must close the stream now because if we are wrapping a ServletOutputStream, // a future gc can commit a stream that used in another thread (very very bad) stream.flush(); stream.close(); writer.dispose(); } /** * Writes a new css sprite rule for the given resource.<p> * * @param uri the resource to use * @param opts the options to use * * @throws IOException if something goes wrong */ public void writeSpriteInclude(String uri, CmsOptimizationSpriteOptions opts) throws IOException { StringBuffer sb = new StringBuffer(); sb.append(opts.getSelector()); sb.append(" { background-image: url("); sb.append(link(uri)); sb.append(");"); if ((opts.getX() != 0) || (opts.getY() != 0)) { sb.append(" background-position: "); sb.append(-opts.getX()); sb.append("px "); sb.append(-opts.getY()); sb.append("px;"); } sb.append(" }"); getJspContext().getOut().println(sb.toString()); } }