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