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>.

viewer-2-initial-appearance
Diva Canto 2010-12-08 18:53:15 -08:00
parent 796216e44f
commit 1070cffcf9
2 changed files with 289 additions and 48 deletions

View File

@ -26,6 +26,7 @@
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
@ -363,5 +364,85 @@ namespace OpenSim.Framework
}
#endregion Stream
public class QBasedComparer : IComparer
{
public int Compare(Object x, Object y)
{
float qx = GetQ(x);
float qy = GetQ(y);
if (qx < qy)
return -1;
if (qx == qy)
return 0;
return 1;
}
private float GetQ(Object o)
{
// Example: image/png;q=0.9
if (o is String)
{
string mime = (string)o;
string[] parts = mime.Split(new char[] { ';' });
if (parts.Length > 1)
{
string[] kvp = parts[1].Split(new char[] { '=' });
if (kvp.Length == 2 && kvp[0] == "q")
{
float qvalue = 1F;
float.TryParse(kvp[1], out qvalue);
return qvalue;
}
}
}
return 1F;
}
}
/// <summary>
/// Takes the value of an Accept header and returns the preferred types
/// ordered by q value (if it exists).
/// Example input: image/jpg;q=0.7, image/png;q=0.8, image/jp2
/// Exmaple output: ["jp2", "png", "jpg"]
/// NOTE: This doesn't handle the semantics of *'s...
/// </summary>
/// <param name="accept"></param>
/// <returns></returns>
public static string[] GetPreferredImageTypes(string accept)
{
if (accept == null || accept == string.Empty)
return new string[0];
string[] types = accept.Split(new char[] { ',' });
if (types.Length > 0)
{
List<string> list = new List<string>(types);
list.RemoveAll(delegate(string s) { return !s.ToLower().StartsWith("image"); });
ArrayList tlist = new ArrayList(list);
tlist.Sort(new QBasedComparer());
string[] result = new string[tlist.Count];
for (int i = 0; i < tlist.Count; i++)
{
string mime = (string)tlist[i];
string[] parts = mime.Split(new char[] { ';' });
string[] pair = parts[0].Split(new char[] { '/' });
if (pair.Length == 2)
result[i] = pair[1].ToLower();
else // oops, we don't know what this is...
result[i] = pair[0];
}
return result;
}
return new string[0];
}
}
}

View File

@ -28,6 +28,8 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Drawing;
using System.Drawing.Imaging;
using System.Reflection;
using System.IO;
using System.Web;
@ -35,6 +37,7 @@ using log4net;
using Nini.Config;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using OpenMetaverse.Imaging;
using OpenSim.Framework;
using OpenSim.Framework.Servers;
using OpenSim.Framework.Servers.HttpServer;
@ -74,6 +77,12 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
private Scene m_scene;
private IAssetService m_assetService;
public const string DefaultFormat = "x-j2c";
// TODO: Change this to a config option
const string REDIRECT_URL = null;
#region IRegionModule Members
public void Initialise(Scene pScene, IConfigSource pSource)
@ -96,7 +105,7 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
{
UUID capID = UUID.Random();
m_log.Info("[GETTEXTURE]: /CAPS/" + capID);
m_log.InfoFormat("[GETTEXTURE]: /CAPS/{0} in region {1}", capID, m_scene.RegionInfo.RegionName);
caps.RegisterHandler("GetTexture", new StreamHandler("GET", "/CAPS/" + capID, ProcessGetTexture));
}
@ -104,12 +113,12 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
private byte[] ProcessGetTexture(string path, Stream request, OSHttpRequest httpRequest, OSHttpResponse httpResponse)
{
// TODO: Change this to a config option
const string REDIRECT_URL = null;
//m_log.DebugFormat("[GETTEXTURE]: called in {0}", m_scene.RegionInfo.RegionName);
// Try to parse the texture ID from the request URL
NameValueCollection query = HttpUtility.ParseQueryString(httpRequest.Url.Query);
string textureStr = query.GetOne("texture_id");
string format = query.GetOne("format");
if (m_assetService == null)
{
@ -121,52 +130,27 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
UUID textureID;
if (!String.IsNullOrEmpty(textureStr) && UUID.TryParse(textureStr, out textureID))
{
//m_log.DebugFormat("[GETTEXTURE]: {0}", textureID);
AssetBase texture;
if (!String.IsNullOrEmpty(REDIRECT_URL))
string[] formats;
if (format != null && format != string.Empty)
{
// Only try to fetch locally cached textures. Misses are redirected
texture = m_assetService.GetCached(textureID.ToString());
if (texture != null)
{
if (texture.Type != (sbyte)AssetType.Texture)
{
httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
httpResponse.Send();
return null;
}
SendTexture(httpRequest, httpResponse, texture);
}
else
{
string textureUrl = REDIRECT_URL + textureID.ToString();
m_log.Debug("[GETTEXTURE]: Redirecting texture request to " + textureUrl);
httpResponse.RedirectLocation = textureUrl;
}
formats = new string[1] { format.ToLower() };
}
else
{
// Fetch locally or remotely. Misses return a 404
texture = m_assetService.Get(textureID.ToString());
formats = WebUtil.GetPreferredImageTypes(httpRequest.Headers.Get("Accept"));
if (formats.Length == 0)
formats = new string[1] { DefaultFormat }; // default
if (texture != null)
{
if (texture.Type != (sbyte)AssetType.Texture)
{
httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
httpResponse.Send();
return null;
}
SendTexture(httpRequest, httpResponse, texture);
}
else
{
m_log.Warn("[GETTEXTURE]: Texture " + textureID + " not found");
httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
}
}
// OK, we have an array with preferred formats, possibly with only one entry
httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
foreach (string f in formats)
{
if (FetchTexture(httpRequest, httpResponse, textureID, f))
break;
}
}
else
{
@ -177,11 +161,105 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
return null;
}
private void SendTexture(OSHttpRequest request, OSHttpResponse response, AssetBase texture)
/// <summary>
///
/// </summary>
/// <param name="httpRequest"></param>
/// <param name="httpResponse"></param>
/// <param name="textureID"></param>
/// <param name="format"></param>
/// <returns>False for "caller try another codec"; true otherwise</returns>
private bool FetchTexture(OSHttpRequest httpRequest, OSHttpResponse httpResponse, UUID textureID, string format)
{
m_log.DebugFormat("[GETTEXTURE]: {0} with requested format {1}", textureID, format);
AssetBase texture;
string fullID = textureID.ToString();
if (format != DefaultFormat)
fullID = fullID + "-" + format;
if (!String.IsNullOrEmpty(REDIRECT_URL))
{
// Only try to fetch locally cached textures. Misses are redirected
texture = m_assetService.GetCached(fullID);
if (texture != null)
{
if (texture.Type != (sbyte)AssetType.Texture)
{
httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
return true;
}
WriteTextureData(httpRequest, httpResponse, texture, format);
}
else
{
string textureUrl = REDIRECT_URL + textureID.ToString();
m_log.Debug("[GETTEXTURE]: Redirecting texture request to " + textureUrl);
httpResponse.RedirectLocation = textureUrl;
return true;
}
}
else // no redirect
{
// try the cache
texture = m_assetService.GetCached(fullID);
if (texture == null)
{
//m_log.DebugFormat("[GETTEXTURE]: texture was not in the cache");
// Fetch locally or remotely. Misses return a 404
texture = m_assetService.Get(textureID.ToString());
if (texture != null)
{
if (texture.Type != (sbyte)AssetType.Texture)
{
httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
return true;
}
if (format == DefaultFormat)
{
WriteTextureData(httpRequest, httpResponse, texture, format);
return true;
}
else
{
AssetBase newTexture = new AssetBase(texture.ID + "-" + format, texture.Name, (sbyte)AssetType.Texture, texture.CreatorID);
newTexture.Data = ConvertTextureData(texture, format);
if (newTexture.Data.Length == 0)
return false; // !!! Caller try another codec, please!
newTexture.Flags = AssetFlags.Collectable;
newTexture.Temporary = true;
m_assetService.Store(newTexture);
WriteTextureData(httpRequest, httpResponse, newTexture, format);
return true;
}
}
}
else // it was on the cache
{
//m_log.DebugFormat("[GETTEXTURE]: texture was in the cache");
WriteTextureData(httpRequest, httpResponse, texture, format);
return true;
}
}
// not found
m_log.Warn("[GETTEXTURE]: Texture " + textureID + " not found");
httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
return true;
}
private void WriteTextureData(OSHttpRequest request, OSHttpResponse response, AssetBase texture, string format)
{
string range = request.Headers.GetOne("Range");
//m_log.DebugFormat("[GETTEXTURE]: Range {0}", range);
if (!String.IsNullOrEmpty(range))
if (!String.IsNullOrEmpty(range)) // JP2's only
{
// Range request
int start, end;
@ -212,15 +290,19 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
}
else
{
m_log.Warn("Malformed Range header: " + range);
m_log.Warn("[GETTEXTURE]: Malformed Range header: " + range);
response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest;
}
}
else
else // JP2's or other formats
{
// Full content request
response.StatusCode = (int)System.Net.HttpStatusCode.OK;
response.ContentLength = texture.Data.Length;
response.ContentType = texture.Metadata.ContentType;
if (format == DefaultFormat)
response.ContentType = texture.Metadata.ContentType;
else
response.ContentType = "image/" + format;
response.Body.Write(texture.Data, 0, texture.Data.Length);
}
}
@ -240,5 +322,83 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
start = end = 0;
return false;
}
private byte[] ConvertTextureData(AssetBase texture, string format)
{
m_log.DebugFormat("[GETTEXTURE]: Converting texture {0} to {1}", texture.ID, format);
byte[] data = new byte[0];
MemoryStream imgstream = new MemoryStream();
Bitmap mTexture = new Bitmap(1, 1);
ManagedImage managedImage;
Image image = (Image)mTexture;
try
{
// Taking our jpeg2000 data, decoding it, then saving it to a byte array with regular jpeg data
imgstream = new MemoryStream();
// Decode image to System.Drawing.Image
if (OpenJPEG.DecodeToImage(texture.Data, out managedImage, out image))
{
// Save to bitmap
mTexture = new Bitmap(image);
EncoderParameters myEncoderParameters = new EncoderParameters();
myEncoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, 95L);
// Save bitmap to stream
ImageCodecInfo codec = GetEncoderInfo("image/" + format);
if (codec != null)
{
mTexture.Save(imgstream, codec, myEncoderParameters);
// Write the stream to a byte array for output
data = imgstream.ToArray();
}
else
m_log.WarnFormat("[GETTEXTURE]: No such codec {0}", format);
}
}
catch (Exception e)
{
m_log.WarnFormat("[GETTEXTURE]: Unable to convert texture {0} to {1}: {2}", texture.ID, format, e.Message);
}
finally
{
// Reclaim memory, these are unmanaged resources
// If we encountered an exception, one or more of these will be null
if (mTexture != null)
mTexture.Dispose();
if (image != null)
image.Dispose();
if (imgstream != null)
{
imgstream.Close();
imgstream.Dispose();
}
}
return data;
}
// From msdn
private static ImageCodecInfo GetEncoderInfo(String mimeType)
{
ImageCodecInfo[] encoders;
encoders = ImageCodecInfo.GetImageEncoders();
for (int j = 0; j < encoders.Length; ++j)
{
if (encoders[j].MimeType == mimeType)
return encoders[j];
}
return null;
}
}
}