001/* 002 * Copyright 2016 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.core.resources; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.OutputStream; 021import java.io.Serializable; 022import java.util.Collection; 023import java.util.Map; 024import java.util.Set; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import org.apache.avalon.framework.parameters.Parameters; 029import org.apache.cocoon.ProcessingException; 030import org.apache.cocoon.ResourceNotFoundException; 031import org.apache.commons.io.IOUtils; 032import org.apache.commons.lang3.StringUtils; 033import org.apache.excalibur.source.Source; 034import org.apache.excalibur.source.SourceResolver; 035 036import org.ametys.core.util.ImageHelper; 037 038/** 039 * Resource handler for images 040 */ 041public class ImageResourceHandler extends SimpleResourceHandler 042{ 043 private static final Pattern _SIZE_PATTERN = Pattern.compile("^(.+)_(max|crop|)(\\d+)x(\\d+)(\\.[^./]+)?$"); 044 045 private static final Collection<String> __ALLOWED_OUTPUT_FORMATS = Set.of("png", "gif", "jpg", "jpeg"); 046 private static final Collection<String> __UNRESIZABLE_FORMATS = Set.of("svg"); 047 048 private int _height; 049 private int _width; 050 private int _maxHeight; 051 private int _maxWidth; 052 private int _cropHeight; 053 private int _cropWidth; 054 055 private boolean _download; 056 057 /** 058 * Default constructor 059 */ 060 public ImageResourceHandler() 061 { 062 super(); 063 } 064 065 /** 066 * If the {@link Source} is already resolved by the {@link ResourceHandlerProvider}, 067 * it may provide it through the constructor to avoid resolving it again. 068 * @param source the source. 069 */ 070 public ImageResourceHandler(Source source) 071 { 072 super(source); 073 } 074 075 @Override 076 public Source setup(String location, Map objectModel, Parameters par, boolean readForDownload) throws IOException, ProcessingException 077 { 078 if (_source == null) 079 { 080 // If the source has not been resolved by the provider, do it now 081 _source = _resolveSource(location, _resolver); 082 } 083 084 if (_source != null) 085 { 086 Matcher sizeMatcher = _SIZE_PATTERN.matcher(location); 087 if (sizeMatcher.matches()) 088 { 089 // type is either empty (resize), max or crop. 090 String type = sizeMatcher.group(2); 091 092 int height = Integer.parseInt(sizeMatcher.group(3)); 093 int width = Integer.parseInt(sizeMatcher.group(4)); 094 095 _height = "".equals(type) ? height : 0; 096 _width = "".equals(type) ? width : 0; 097 _maxHeight = "max".equals(type) ? height : 0; 098 _maxWidth = "max".equals(type) ? width : 0; 099 _cropHeight = "crop".equals(type) ? height : 0; 100 _cropWidth = "crop".equals(type) ? width : 0; 101 } 102 103 _download = readForDownload; 104 105 return _source; 106 } 107 else 108 { 109 throw new ResourceNotFoundException("Resource not found for URI : " + location); 110 } 111 } 112 113 @Override 114 public void generate(OutputStream out) throws IOException, ProcessingException 115 { 116 String fileExtension = StringUtils.substringAfterLast(_source.getURI(), ".").toLowerCase(); 117 118 try (InputStream is = _source.getInputStream()) 119 { 120 if (_processImage(fileExtension)) 121 { 122 String outputFormat = __ALLOWED_OUTPUT_FORMATS.contains(fileExtension) ? fileExtension : "png"; 123 ImageHelper.generateThumbnail(is, out, outputFormat, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth); 124 } 125 else 126 { 127 // Copy data in response 128 IOUtils.copy(is, out); 129 } 130 } 131 } 132 133 private boolean _processImage(String fileExtension) 134 { 135 if (__UNRESIZABLE_FORMATS.contains(fileExtension)) 136 { 137 return false; 138 } 139 else if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0) 140 { 141 // resize or crop is required, assume this is an image 142 return true; 143 } 144 else if (!_download) 145 { 146 String mimeType = _source.getMimeType(); 147 148 // only process image if it is for rendering purposes 149 return mimeType != null && mimeType.startsWith("image/"); 150 } 151 else 152 { 153 return false; 154 } 155 } 156 157 @Override 158 public Serializable getKey() 159 { 160 return _source.getURI() + "###" + _width + "x" + _height + "x" + _maxWidth + "x" + _maxHeight + "x" + _cropWidth + "x" + _cropHeight; 161 } 162 163 /** 164 * Resolve the source at the given location 165 * @param location the location of the source to resolve 166 * @param resolver the source resolver 167 * @return the resolved source 168 */ 169 static Source _resolveSource(String location, SourceResolver resolver) 170 { 171 try 172 { 173 Source source = resolver.resolveURI(location); 174 if (source == null || !source.exists()) 175 { 176 resolver.release(source); 177 178 Matcher sizeMatcher = _SIZE_PATTERN.matcher(location); 179 if (sizeMatcher.matches()) 180 { 181 String computedLocation = sizeMatcher.group(1); 182 String suffix = sizeMatcher.group(5); 183 if (suffix != null) 184 { 185 computedLocation += suffix; 186 } 187 188 source = resolver.resolveURI(computedLocation); 189 if (!source.exists()) 190 { 191 resolver.release(source); 192 } 193 } 194 } 195 196 return source != null && source.exists() ? source : null; 197 } 198 catch (IOException e) 199 { 200 return null; 201 } 202 } 203}