001/* 002 * Copyright 2010 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.skinfactory.readers; 017 018import java.awt.Dimension; 019import java.awt.image.BufferedImage; 020import java.io.ByteArrayInputStream; 021import java.io.File; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.io.Serializable; 026import java.io.UnsupportedEncodingException; 027import java.net.URLDecoder; 028import java.util.Map; 029 030import javax.imageio.ImageIO; 031 032import org.apache.avalon.framework.parameters.Parameters; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.cocoon.ProcessingException; 036import org.apache.cocoon.caching.CacheableProcessingComponent; 037import org.apache.cocoon.environment.ObjectModelHelper; 038import org.apache.cocoon.environment.Response; 039import org.apache.cocoon.environment.SourceResolver; 040import org.apache.cocoon.reading.ServiceableReader; 041import org.apache.commons.io.IOUtils; 042import org.apache.commons.lang.StringUtils; 043import org.apache.excalibur.source.SourceValidity; 044import org.apache.excalibur.source.impl.FileSource; 045import org.xml.sax.SAXException; 046 047import org.ametys.core.util.ImageHelper; 048import org.ametys.plugins.skincommons.SkinEditionHelper; 049import org.ametys.web.skin.SkinModel; 050import org.ametys.web.skin.SkinModelsManager; 051 052import net.coobird.thumbnailator.makers.FixedSizeThumbnailMaker; 053import net.coobird.thumbnailator.resizers.DefaultResizerFactory; 054 055/** 056 * Reader for resource of the skin 057 */ 058public class SkinResourceReader extends ServiceableReader implements CacheableProcessingComponent 059{ 060 private SkinModelsManager _modelsManager; 061 private SkinEditionHelper _skinHelper; 062 063 private FileSource _source; 064 065 private int _width; 066 private int _height; 067 private int _maxWidth; 068 private int _maxHeight; 069 070 @Override 071 public void service(ServiceManager sManager) throws ServiceException 072 { 073 super.service(sManager); 074 _modelsManager = (SkinModelsManager) sManager.lookup(SkinModelsManager.ROLE); 075 _skinHelper = (SkinEditionHelper) sManager.lookup(SkinEditionHelper.ROLE); 076 } 077 078 @Override 079 public void setup(SourceResolver sResolver, Map objModel, String src, Parameters par) throws ProcessingException, SAXException, IOException 080 { 081 super.setup(sResolver, objModel, src, par); 082 083 // parameters for image resizing 084 _width = par.getParameterAsInteger("width", 0); 085 _height = par.getParameterAsInteger("height", 0); 086 _maxWidth = par.getParameterAsInteger("maxWidth", 0); 087 _maxHeight = par.getParameterAsInteger("maxHeight", 0); 088 089 String path = par.getParameter("path", null); 090 assert path != null; 091 092 File rootDir = null; 093 String modelName = par.getParameter("modelName", null); 094 if (StringUtils.isNotEmpty(modelName)) 095 { 096 SkinModel model = _modelsManager.getModel(modelName); 097 rootDir = model.getFile(); 098 } 099 else 100 { 101 String skinName = par.getParameter("skinName", null); 102 rootDir = _skinHelper.getTempDirectory(skinName); 103 } 104 105 _source = new FileSource("file", new File(rootDir, _decodePath(path))); 106 } 107 108 /** 109 * Decode the resource path 110 * @param path the resource path 111 * @return the decoded resource path 112 * @throws UnsupportedEncodingException if UTF-8 encoding is not supported 113 */ 114 protected String _decodePath (String path) throws UnsupportedEncodingException 115 { 116 StringBuffer sb = new StringBuffer(); 117 118 String[] parts = path.split("/"); 119 for (String part : parts) 120 { 121 sb.append("/"); 122 sb.append(URLDecoder.decode(part, "utf-8")); 123 } 124 return sb.toString(); 125 } 126 127 @Override 128 public Serializable getKey() 129 { 130 return _source.getFile().getAbsolutePath() + "#" + _height + "#" + _width + "#" + _maxHeight + "#" + _maxWidth; 131 } 132 133 @Override 134 public SourceValidity getValidity() 135 { 136 return _source.getValidity(); 137 } 138 139 @Override 140 public long getLastModified() 141 { 142 return _source.getLastModified(); 143 } 144 145 @Override 146 public String getMimeType() 147 { 148 return _source.getMimeType(); 149 } 150 151 @Override 152 public void generate() throws IOException, SAXException, ProcessingException 153 { 154 String name = _source.getName(); 155 name = name.replaceAll("\\\\", "\\\\\\\\"); 156 name = name.replaceAll("\\\"", "\\\\\\\""); 157 158 Response response = ObjectModelHelper.getResponse(objectModel); 159 160 try (InputStream is = _source.getInputStream()) 161 { 162 if (_width > 0 || _height > 0) 163 { 164 // it's an image, which must be resized 165 int i = name.lastIndexOf('.'); 166 String format = name.substring(i + 1); 167 168 ImageHelper.generateThumbnail(is, out, format, _height, _width, 0, 0); 169 } 170 else if (_maxHeight > 0 || _maxWidth > 0) 171 { 172 // it's an image, which must be resized 173 int i = name.lastIndexOf('.'); 174 String format = name.substring(i + 1); 175 176 byte[] fileContent = IOUtils.toByteArray(is); 177 BufferedImage src = ImageHelper.read(new ByteArrayInputStream(fileContent)); 178 179 _generateThumbnail (out, format, src, fileContent, _maxHeight, _maxWidth); 180 } 181 else 182 { 183 response.setHeader("Content-Length", Long.toString(_source.getContentLength())); 184 IOUtils.copy(is, out); 185 } 186 } 187 catch (Exception e) 188 { 189 throw new ProcessingException("Unable to download file of uri " + _source.getURI(), e); 190 } 191 finally 192 { 193 IOUtils.closeQuietly(out); 194 } 195 } 196 197 @Override 198 public void recycle() 199 { 200 super.recycle(); 201 _source = null; 202 } 203 204 private void _generateThumbnail (OutputStream os, String format, BufferedImage src, byte[] fileContent, int maxHeight, int maxWidth) throws IOException 205 { 206 int srcHeight = src.getHeight(); 207 int srcWidth = src.getWidth(); 208 209 if (maxWidth == srcWidth && maxHeight == srcHeight) 210 { 211 IOUtils.write(fileContent, os); 212 return; 213 } 214 if (srcWidth < maxWidth && srcHeight < maxHeight) 215 { 216 // Image is too small : zoom them crop image to minHeight x minWidth dimension 217 _generateZoomAndCropImage (out, format, src, fileContent, maxHeight, maxWidth); 218 } 219 else 220 { 221 BufferedImage dest = ImageHelper.generateThumbnail(src, _height, _width, _maxHeight, _maxWidth); 222 if (src == dest) 223 { 224 // Thumbnail is equals to src image, means that the image is the same 225 // We'd rather like return the initial stream 226 IOUtils.write(fileContent, os); 227 } 228 else 229 { 230 ImageIO.write(dest, format, os); 231 } 232 } 233 } 234 235 private void _generateZoomAndCropImage (OutputStream os, String format, BufferedImage src, byte[] fileContent, int minHeight, int minWidth) throws IOException 236 { 237 int srcHeight = src.getHeight(); 238 int srcWidth = src.getWidth(); 239 240 int destHeight = 0; 241 int destWidth = 0; 242 243 Dimension srcDimension = new Dimension(srcWidth, srcHeight); 244 245 boolean keepAspectRatio = true; 246 247 if (srcWidth > srcHeight) 248 { 249 destHeight = minHeight; 250 251 // width is computed keeping ratio 252 destWidth = srcWidth * destHeight / srcHeight; 253 } 254 else 255 { 256 destWidth = minWidth; 257 258 // dest is computed keeping ratio 259 destHeight = srcHeight * destWidth / srcWidth; 260 } 261 262 Dimension thumbnailDimension = new Dimension(destWidth, destHeight); 263 264 265 BufferedImage thumbImage = new FixedSizeThumbnailMaker(destWidth, destHeight, keepAspectRatio, true) 266 .resizer(DefaultResizerFactory.getInstance().getResizer(srcDimension, thumbnailDimension)) 267 .imageType(src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB) 268 .make(src); 269 270 BufferedImage cropImage = _getCropImage (thumbImage, 0, 0, minHeight, minHeight); 271 272 if (src == cropImage) 273 { 274 // Thumbnail is equals to src image, means that the image is the same 275 // We'd rather like return the initial stream 276 IOUtils.write(fileContent, os); 277 } 278 else 279 { 280 ImageIO.write(cropImage, format, os); 281 } 282 } 283 284 private BufferedImage _getCropImage (BufferedImage src, int x, int y, int width, int height) 285 { 286 int srcHeight = src.getHeight(); 287 int srcWidth = src.getWidth(); 288 289 int w = width; 290 if (width + x > srcWidth) 291 { 292 w = srcWidth - x; 293 } 294 295 int h = height; 296 if (height + y > srcHeight) 297 { 298 h = srcHeight - y; 299 } 300 301 return src.getSubimage(x, y, w, h); 302 } 303}