diff options
author | Diva Canto | 2010-12-08 18:53:15 -0800 |
---|---|---|
committer | Diva Canto | 2010-12-08 18:53:15 -0800 |
commit | 1070cffcf905f77d694e140d0c9e978f5d0052c0 (patch) | |
tree | 76ae656390dd231d8c221ebff0891fe04782a400 | |
parent | Added an exception handler on CreateObject handler, just in case there's an e... (diff) | |
download | opensim-SC_OLD-1070cffcf905f77d694e140d0c9e978f5d0052c0.zip opensim-SC_OLD-1070cffcf905f77d694e140d0c9e978f5d0052c0.tar.gz opensim-SC_OLD-1070cffcf905f77d694e140d0c9e978f5d0052c0.tar.bz2 opensim-SC_OLD-1070cffcf905f77d694e140d0c9e978f5d0052c0.tar.xz |
Added ability for GetTexture to serve multiple formats. The format may come as an extra query parameter in the URL format=<format> (this was tested and working) or it may come in the Accept header (code added, but not tested). The result of the conversion is placed in the asset cache, under the name <uuid>-<format>.
-rw-r--r-- | OpenSim/Framework/WebUtil.cs | 81 | ||||
-rw-r--r-- | OpenSim/Region/CoreModules/Avatar/Assets/GetTextureModule.cs | 244 |
2 files changed, 283 insertions, 42 deletions
diff --git a/OpenSim/Framework/WebUtil.cs b/OpenSim/Framework/WebUtil.cs index d16f9bf..1c856af 100644 --- a/OpenSim/Framework/WebUtil.cs +++ b/OpenSim/Framework/WebUtil.cs | |||
@@ -26,6 +26,7 @@ | |||
26 | */ | 26 | */ |
27 | 27 | ||
28 | using System; | 28 | using System; |
29 | using System.Collections; | ||
29 | using System.Collections.Generic; | 30 | using System.Collections.Generic; |
30 | using System.Collections.Specialized; | 31 | using System.Collections.Specialized; |
31 | using System.IO; | 32 | using System.IO; |
@@ -363,5 +364,85 @@ namespace OpenSim.Framework | |||
363 | } | 364 | } |
364 | 365 | ||
365 | #endregion Stream | 366 | #endregion Stream |
367 | |||
368 | public class QBasedComparer : IComparer | ||
369 | { | ||
370 | public int Compare(Object x, Object y) | ||
371 | { | ||
372 | float qx = GetQ(x); | ||
373 | float qy = GetQ(y); | ||
374 | if (qx < qy) | ||
375 | return -1; | ||
376 | if (qx == qy) | ||
377 | return 0; | ||
378 | return 1; | ||
379 | } | ||
380 | |||
381 | private float GetQ(Object o) | ||
382 | { | ||
383 | // Example: image/png;q=0.9 | ||
384 | |||
385 | if (o is String) | ||
386 | { | ||
387 | string mime = (string)o; | ||
388 | string[] parts = mime.Split(new char[] { ';' }); | ||
389 | if (parts.Length > 1) | ||
390 | { | ||
391 | string[] kvp = parts[1].Split(new char[] { '=' }); | ||
392 | if (kvp.Length == 2 && kvp[0] == "q") | ||
393 | { | ||
394 | float qvalue = 1F; | ||
395 | float.TryParse(kvp[1], out qvalue); | ||
396 | return qvalue; | ||
397 | } | ||
398 | } | ||
399 | } | ||
400 | |||
401 | return 1F; | ||
402 | } | ||
403 | } | ||
404 | |||
405 | /// <summary> | ||
406 | /// Takes the value of an Accept header and returns the preferred types | ||
407 | /// ordered by q value (if it exists). | ||
408 | /// Example input: image/jpg;q=0.7, image/png;q=0.8, image/jp2 | ||
409 | /// Exmaple output: ["jp2", "png", "jpg"] | ||
410 | /// NOTE: This doesn't handle the semantics of *'s... | ||
411 | /// </summary> | ||
412 | /// <param name="accept"></param> | ||
413 | /// <returns></returns> | ||
414 | public static string[] GetPreferredImageTypes(string accept) | ||
415 | { | ||
416 | |||
417 | if (accept == null || accept == string.Empty) | ||
418 | return new string[0]; | ||
419 | |||
420 | string[] types = accept.Split(new char[] { ',' }); | ||
421 | if (types.Length > 0) | ||
422 | { | ||
423 | List<string> list = new List<string>(types); | ||
424 | list.RemoveAll(delegate(string s) { return !s.ToLower().StartsWith("image"); }); | ||
425 | ArrayList tlist = new ArrayList(list); | ||
426 | tlist.Sort(new QBasedComparer()); | ||
427 | |||
428 | string[] result = new string[tlist.Count]; | ||
429 | for (int i = 0; i < tlist.Count; i++) | ||
430 | { | ||
431 | string mime = (string)tlist[i]; | ||
432 | string[] parts = mime.Split(new char[] { ';' }); | ||
433 | string[] pair = parts[0].Split(new char[] { '/' }); | ||
434 | if (pair.Length == 2) | ||
435 | result[i] = pair[1].ToLower(); | ||
436 | else // oops, we don't know what this is... | ||
437 | result[i] = pair[0]; | ||
438 | } | ||
439 | |||
440 | return result; | ||
441 | } | ||
442 | |||
443 | return new string[0]; | ||
444 | } | ||
445 | |||
446 | |||
366 | } | 447 | } |
367 | } | 448 | } |
diff --git a/OpenSim/Region/CoreModules/Avatar/Assets/GetTextureModule.cs b/OpenSim/Region/CoreModules/Avatar/Assets/GetTextureModule.cs index 97581e5..ac2ad7a 100644 --- a/OpenSim/Region/CoreModules/Avatar/Assets/GetTextureModule.cs +++ b/OpenSim/Region/CoreModules/Avatar/Assets/GetTextureModule.cs | |||
@@ -28,6 +28,8 @@ | |||
28 | using System; | 28 | using System; |
29 | using System.Collections; | 29 | using System.Collections; |
30 | using System.Collections.Specialized; | 30 | using System.Collections.Specialized; |
31 | using System.Drawing; | ||
32 | using System.Drawing.Imaging; | ||
31 | using System.Reflection; | 33 | using System.Reflection; |
32 | using System.IO; | 34 | using System.IO; |
33 | using System.Web; | 35 | using System.Web; |
@@ -35,6 +37,7 @@ using log4net; | |||
35 | using Nini.Config; | 37 | using Nini.Config; |
36 | using OpenMetaverse; | 38 | using OpenMetaverse; |
37 | using OpenMetaverse.StructuredData; | 39 | using OpenMetaverse.StructuredData; |
40 | using OpenMetaverse.Imaging; | ||
38 | using OpenSim.Framework; | 41 | using OpenSim.Framework; |
39 | using OpenSim.Framework.Servers; | 42 | using OpenSim.Framework.Servers; |
40 | using OpenSim.Framework.Servers.HttpServer; | 43 | using OpenSim.Framework.Servers.HttpServer; |
@@ -74,6 +77,12 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps | |||
74 | private Scene m_scene; | 77 | private Scene m_scene; |
75 | private IAssetService m_assetService; | 78 | private IAssetService m_assetService; |
76 | 79 | ||
80 | public const string DefaultFormat = "x-j2c"; | ||
81 | |||
82 | // TODO: Change this to a config option | ||
83 | const string REDIRECT_URL = null; | ||
84 | |||
85 | |||
77 | #region IRegionModule Members | 86 | #region IRegionModule Members |
78 | 87 | ||
79 | public void Initialise(Scene pScene, IConfigSource pSource) | 88 | public void Initialise(Scene pScene, IConfigSource pSource) |
@@ -96,7 +105,7 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps | |||
96 | { | 105 | { |
97 | UUID capID = UUID.Random(); | 106 | UUID capID = UUID.Random(); |
98 | 107 | ||
99 | m_log.Info("[GETTEXTURE]: /CAPS/" + capID); | 108 | m_log.InfoFormat("[GETTEXTURE]: /CAPS/{0} in region {1}", capID, m_scene.RegionInfo.RegionName); |
100 | caps.RegisterHandler("GetTexture", new StreamHandler("GET", "/CAPS/" + capID, ProcessGetTexture)); | 109 | caps.RegisterHandler("GetTexture", new StreamHandler("GET", "/CAPS/" + capID, ProcessGetTexture)); |
101 | } | 110 | } |
102 | 111 | ||
@@ -104,12 +113,12 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps | |||
104 | 113 | ||
105 | private byte[] ProcessGetTexture(string path, Stream request, OSHttpRequest httpRequest, OSHttpResponse httpResponse) | 114 | private byte[] ProcessGetTexture(string path, Stream request, OSHttpRequest httpRequest, OSHttpResponse httpResponse) |
106 | { | 115 | { |
107 | // TODO: Change this to a config option | 116 | //m_log.DebugFormat("[GETTEXTURE]: called in {0}", m_scene.RegionInfo.RegionName); |
108 | const string REDIRECT_URL = null; | ||
109 | 117 | ||
110 | // Try to parse the texture ID from the request URL | 118 | // Try to parse the texture ID from the request URL |
111 | NameValueCollection query = HttpUtility.ParseQueryString(httpRequest.Url.Query); | 119 | NameValueCollection query = HttpUtility.ParseQueryString(httpRequest.Url.Query); |
112 | string textureStr = query.GetOne("texture_id"); | 120 | string textureStr = query.GetOne("texture_id"); |
121 | string format = query.GetOne("format"); | ||
113 | 122 | ||
114 | if (m_assetService == null) | 123 | if (m_assetService == null) |
115 | { | 124 | { |
@@ -121,33 +130,85 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps | |||
121 | UUID textureID; | 130 | UUID textureID; |
122 | if (!String.IsNullOrEmpty(textureStr) && UUID.TryParse(textureStr, out textureID)) | 131 | if (!String.IsNullOrEmpty(textureStr) && UUID.TryParse(textureStr, out textureID)) |
123 | { | 132 | { |
124 | //m_log.DebugFormat("[GETTEXTURE]: {0}", textureID); | 133 | string[] formats; |
125 | AssetBase texture; | 134 | if (format != null && format != string.Empty) |
135 | { | ||
136 | formats = new string[1] { format.ToLower() }; | ||
137 | } | ||
138 | else | ||
139 | { | ||
140 | formats = WebUtil.GetPreferredImageTypes(httpRequest.Headers.Get("Accept")); | ||
141 | if (formats.Length == 0) | ||
142 | formats = new string[1] { DefaultFormat }; // default | ||
126 | 143 | ||
127 | if (!String.IsNullOrEmpty(REDIRECT_URL)) | 144 | } |
145 | // OK, we have an array with preferred formats, possibly with only one entry | ||
146 | |||
147 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; | ||
148 | foreach (string f in formats) | ||
128 | { | 149 | { |
129 | // Only try to fetch locally cached textures. Misses are redirected | 150 | if (FetchTexture(httpRequest, httpResponse, textureID, f)) |
130 | texture = m_assetService.GetCached(textureID.ToString()); | 151 | break; |
152 | } | ||
131 | 153 | ||
132 | if (texture != null) | 154 | } |
133 | { | 155 | else |
134 | if (texture.Type != (sbyte)AssetType.Texture) | 156 | { |
135 | { | 157 | m_log.Warn("[GETTEXTURE]: Failed to parse a texture_id from GetTexture request: " + httpRequest.Url); |
136 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; | 158 | } |
137 | httpResponse.Send(); | 159 | |
138 | return null; | 160 | httpResponse.Send(); |
139 | } | 161 | return null; |
140 | SendTexture(httpRequest, httpResponse, texture); | 162 | } |
141 | } | 163 | |
142 | else | 164 | /// <summary> |
165 | /// | ||
166 | /// </summary> | ||
167 | /// <param name="httpRequest"></param> | ||
168 | /// <param name="httpResponse"></param> | ||
169 | /// <param name="textureID"></param> | ||
170 | /// <param name="format"></param> | ||
171 | /// <returns>False for "caller try another codec"; true otherwise</returns> | ||
172 | private bool FetchTexture(OSHttpRequest httpRequest, OSHttpResponse httpResponse, UUID textureID, string format) | ||
173 | { | ||
174 | m_log.DebugFormat("[GETTEXTURE]: {0} with requested format {1}", textureID, format); | ||
175 | AssetBase texture; | ||
176 | |||
177 | string fullID = textureID.ToString(); | ||
178 | if (format != DefaultFormat) | ||
179 | fullID = fullID + "-" + format; | ||
180 | |||
181 | if (!String.IsNullOrEmpty(REDIRECT_URL)) | ||
182 | { | ||
183 | // Only try to fetch locally cached textures. Misses are redirected | ||
184 | texture = m_assetService.GetCached(fullID); | ||
185 | |||
186 | if (texture != null) | ||
187 | { | ||
188 | if (texture.Type != (sbyte)AssetType.Texture) | ||
143 | { | 189 | { |
144 | string textureUrl = REDIRECT_URL + textureID.ToString(); | 190 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; |
145 | m_log.Debug("[GETTEXTURE]: Redirecting texture request to " + textureUrl); | 191 | return true; |
146 | httpResponse.RedirectLocation = textureUrl; | ||
147 | } | 192 | } |
193 | WriteTextureData(httpRequest, httpResponse, texture, format); | ||
148 | } | 194 | } |
149 | else | 195 | else |
150 | { | 196 | { |
197 | string textureUrl = REDIRECT_URL + textureID.ToString(); | ||
198 | m_log.Debug("[GETTEXTURE]: Redirecting texture request to " + textureUrl); | ||
199 | httpResponse.RedirectLocation = textureUrl; | ||
200 | return true; | ||
201 | } | ||
202 | } | ||
203 | else // no redirect | ||
204 | { | ||
205 | // try the cache | ||
206 | texture = m_assetService.GetCached(fullID); | ||
207 | |||
208 | if (texture == null) | ||
209 | { | ||
210 | //m_log.DebugFormat("[GETTEXTURE]: texture was not in the cache"); | ||
211 | |||
151 | // Fetch locally or remotely. Misses return a 404 | 212 | // Fetch locally or remotely. Misses return a 404 |
152 | texture = m_assetService.Get(textureID.ToString()); | 213 | texture = m_assetService.Get(textureID.ToString()); |
153 | 214 | ||
@@ -156,32 +217,49 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps | |||
156 | if (texture.Type != (sbyte)AssetType.Texture) | 217 | if (texture.Type != (sbyte)AssetType.Texture) |
157 | { | 218 | { |
158 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; | 219 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; |
159 | httpResponse.Send(); | 220 | return true; |
160 | return null; | 221 | } |
222 | if (format == DefaultFormat) | ||
223 | { | ||
224 | WriteTextureData(httpRequest, httpResponse, texture, format); | ||
225 | return true; | ||
226 | } | ||
227 | else | ||
228 | { | ||
229 | AssetBase newTexture = new AssetBase(texture.ID + "-" + format, texture.Name, (sbyte)AssetType.Texture, texture.CreatorID); | ||
230 | newTexture.Data = ConvertTextureData(texture, format); | ||
231 | if (newTexture.Data.Length == 0) | ||
232 | return false; // !!! Caller try another codec, please! | ||
233 | |||
234 | newTexture.Flags = AssetFlags.Collectable; | ||
235 | newTexture.Temporary = true; | ||
236 | m_assetService.Store(newTexture); | ||
237 | WriteTextureData(httpRequest, httpResponse, newTexture, format); | ||
238 | return true; | ||
161 | } | 239 | } |
162 | SendTexture(httpRequest, httpResponse, texture); | ||
163 | } | ||
164 | else | ||
165 | { | ||
166 | m_log.Warn("[GETTEXTURE]: Texture " + textureID + " not found"); | ||
167 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; | ||
168 | } | 240 | } |
169 | } | 241 | } |
170 | } | 242 | else // it was on the cache |
171 | else | 243 | { |
172 | { | 244 | //m_log.DebugFormat("[GETTEXTURE]: texture was in the cache"); |
173 | m_log.Warn("[GETTEXTURE]: Failed to parse a texture_id from GetTexture request: " + httpRequest.Url); | 245 | WriteTextureData(httpRequest, httpResponse, texture, format); |
246 | return true; | ||
247 | } | ||
248 | |||
174 | } | 249 | } |
175 | 250 | ||
176 | httpResponse.Send(); | 251 | // not found |
177 | return null; | 252 | m_log.Warn("[GETTEXTURE]: Texture " + textureID + " not found"); |
253 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; | ||
254 | return true; | ||
255 | |||
178 | } | 256 | } |
179 | 257 | ||
180 | private void SendTexture(OSHttpRequest request, OSHttpResponse response, AssetBase texture) | 258 | private void WriteTextureData(OSHttpRequest request, OSHttpResponse response, AssetBase texture, string format) |
181 | { | 259 | { |
182 | string range = request.Headers.GetOne("Range"); | 260 | string range = request.Headers.GetOne("Range"); |
183 | //m_log.DebugFormat("[GETTEXTURE]: Range {0}", range); | 261 | //m_log.DebugFormat("[GETTEXTURE]: Range {0}", range); |
184 | if (!String.IsNullOrEmpty(range)) | 262 | if (!String.IsNullOrEmpty(range)) // JP2's only |
185 | { | 263 | { |
186 | // Range request | 264 | // Range request |
187 | int start, end; | 265 | int start, end; |
@@ -212,15 +290,19 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps | |||
212 | } | 290 | } |
213 | else | 291 | else |
214 | { | 292 | { |
215 | m_log.Warn("Malformed Range header: " + range); | 293 | m_log.Warn("[GETTEXTURE]: Malformed Range header: " + range); |
216 | response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest; | 294 | response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest; |
217 | } | 295 | } |
218 | } | 296 | } |
219 | else | 297 | else // JP2's or other formats |
220 | { | 298 | { |
221 | // Full content request | 299 | // Full content request |
300 | response.StatusCode = (int)System.Net.HttpStatusCode.OK; | ||
222 | response.ContentLength = texture.Data.Length; | 301 | response.ContentLength = texture.Data.Length; |
223 | response.ContentType = texture.Metadata.ContentType; | 302 | if (format == DefaultFormat) |
303 | response.ContentType = texture.Metadata.ContentType; | ||
304 | else | ||
305 | response.ContentType = "image/" + format; | ||
224 | response.Body.Write(texture.Data, 0, texture.Data.Length); | 306 | response.Body.Write(texture.Data, 0, texture.Data.Length); |
225 | } | 307 | } |
226 | } | 308 | } |
@@ -240,5 +322,83 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps | |||
240 | start = end = 0; | 322 | start = end = 0; |
241 | return false; | 323 | return false; |
242 | } | 324 | } |
325 | |||
326 | |||
327 | private byte[] ConvertTextureData(AssetBase texture, string format) | ||
328 | { | ||
329 | m_log.DebugFormat("[GETTEXTURE]: Converting texture {0} to {1}", texture.ID, format); | ||
330 | byte[] data = new byte[0]; | ||
331 | |||
332 | MemoryStream imgstream = new MemoryStream(); | ||
333 | Bitmap mTexture = new Bitmap(1, 1); | ||
334 | ManagedImage managedImage; | ||
335 | Image image = (Image)mTexture; | ||
336 | |||
337 | try | ||
338 | { | ||
339 | // Taking our jpeg2000 data, decoding it, then saving it to a byte array with regular jpeg data | ||
340 | |||
341 | imgstream = new MemoryStream(); | ||
342 | |||
343 | // Decode image to System.Drawing.Image | ||
344 | if (OpenJPEG.DecodeToImage(texture.Data, out managedImage, out image)) | ||
345 | { | ||
346 | // Save to bitmap | ||
347 | mTexture = new Bitmap(image); | ||
348 | |||
349 | EncoderParameters myEncoderParameters = new EncoderParameters(); | ||
350 | myEncoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, 95L); | ||
351 | |||
352 | // Save bitmap to stream | ||
353 | ImageCodecInfo codec = GetEncoderInfo("image/" + format); | ||
354 | if (codec != null) | ||
355 | { | ||
356 | mTexture.Save(imgstream, codec, myEncoderParameters); | ||
357 | // Write the stream to a byte array for output | ||
358 | data = imgstream.ToArray(); | ||
359 | } | ||
360 | else | ||
361 | m_log.WarnFormat("[GETTEXTURE]: No such codec {0}", format); | ||
362 | |||
363 | } | ||
364 | } | ||
365 | catch (Exception e) | ||
366 | { | ||
367 | m_log.WarnFormat("[GETTEXTURE]: Unable to convert texture {0} to {1}: {2}", texture.ID, format, e.Message); | ||
368 | } | ||
369 | finally | ||
370 | { | ||
371 | // Reclaim memory, these are unmanaged resources | ||
372 | // If we encountered an exception, one or more of these will be null | ||
373 | if (mTexture != null) | ||
374 | mTexture.Dispose(); | ||
375 | |||
376 | if (image != null) | ||
377 | image.Dispose(); | ||
378 | |||
379 | if (imgstream != null) | ||
380 | { | ||
381 | imgstream.Close(); | ||
382 | imgstream.Dispose(); | ||
383 | } | ||
384 | } | ||
385 | |||
386 | return data; | ||
387 | } | ||
388 | |||
389 | // From msdn | ||
390 | private static ImageCodecInfo GetEncoderInfo(String mimeType) | ||
391 | { | ||
392 | ImageCodecInfo[] encoders; | ||
393 | encoders = ImageCodecInfo.GetImageEncoders(); | ||
394 | for (int j = 0; j < encoders.Length; ++j) | ||
395 | { | ||
396 | if (encoders[j].MimeType == mimeType) | ||
397 | return encoders[j]; | ||
398 | } | ||
399 | return null; | ||
400 | } | ||
401 | |||
402 | |||
243 | } | 403 | } |
244 | } | 404 | } |