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