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>.
parent
796216e44f
commit
1070cffcf9
|
@ -26,6 +26,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -363,5 +364,85 @@ namespace OpenSim.Framework
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Stream
|
#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];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
@ -35,6 +37,7 @@ using log4net;
|
||||||
using Nini.Config;
|
using Nini.Config;
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
using OpenMetaverse.StructuredData;
|
using OpenMetaverse.StructuredData;
|
||||||
|
using OpenMetaverse.Imaging;
|
||||||
using OpenSim.Framework;
|
using OpenSim.Framework;
|
||||||
using OpenSim.Framework.Servers;
|
using OpenSim.Framework.Servers;
|
||||||
using OpenSim.Framework.Servers.HttpServer;
|
using OpenSim.Framework.Servers.HttpServer;
|
||||||
|
@ -74,6 +77,12 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
|
||||||
private Scene m_scene;
|
private Scene m_scene;
|
||||||
private IAssetService m_assetService;
|
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
|
#region IRegionModule Members
|
||||||
|
|
||||||
public void Initialise(Scene pScene, IConfigSource pSource)
|
public void Initialise(Scene pScene, IConfigSource pSource)
|
||||||
|
@ -96,7 +105,7 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
|
||||||
{
|
{
|
||||||
UUID capID = UUID.Random();
|
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));
|
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)
|
private byte[] ProcessGetTexture(string path, Stream request, OSHttpRequest httpRequest, OSHttpResponse httpResponse)
|
||||||
{
|
{
|
||||||
// TODO: Change this to a config option
|
//m_log.DebugFormat("[GETTEXTURE]: called in {0}", m_scene.RegionInfo.RegionName);
|
||||||
const string REDIRECT_URL = null;
|
|
||||||
|
|
||||||
// Try to parse the texture ID from the request URL
|
// Try to parse the texture ID from the request URL
|
||||||
NameValueCollection query = HttpUtility.ParseQueryString(httpRequest.Url.Query);
|
NameValueCollection query = HttpUtility.ParseQueryString(httpRequest.Url.Query);
|
||||||
string textureStr = query.GetOne("texture_id");
|
string textureStr = query.GetOne("texture_id");
|
||||||
|
string format = query.GetOne("format");
|
||||||
|
|
||||||
if (m_assetService == null)
|
if (m_assetService == null)
|
||||||
{
|
{
|
||||||
|
@ -121,52 +130,27 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
|
||||||
UUID textureID;
|
UUID textureID;
|
||||||
if (!String.IsNullOrEmpty(textureStr) && UUID.TryParse(textureStr, out textureID))
|
if (!String.IsNullOrEmpty(textureStr) && UUID.TryParse(textureStr, out textureID))
|
||||||
{
|
{
|
||||||
//m_log.DebugFormat("[GETTEXTURE]: {0}", textureID);
|
string[] formats;
|
||||||
AssetBase texture;
|
if (format != null && format != string.Empty)
|
||||||
|
|
||||||
if (!String.IsNullOrEmpty(REDIRECT_URL))
|
|
||||||
{
|
{
|
||||||
// Only try to fetch locally cached textures. Misses are redirected
|
formats = new string[1] { format.ToLower() };
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fetch locally or remotely. Misses return a 404
|
formats = WebUtil.GetPreferredImageTypes(httpRequest.Headers.Get("Accept"));
|
||||||
texture = m_assetService.Get(textureID.ToString());
|
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
|
else
|
||||||
{
|
{
|
||||||
|
@ -177,11 +161,105 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
|
||||||
return null;
|
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");
|
string range = request.Headers.GetOne("Range");
|
||||||
//m_log.DebugFormat("[GETTEXTURE]: Range {0}", range);
|
//m_log.DebugFormat("[GETTEXTURE]: Range {0}", range);
|
||||||
if (!String.IsNullOrEmpty(range))
|
if (!String.IsNullOrEmpty(range)) // JP2's only
|
||||||
{
|
{
|
||||||
// Range request
|
// Range request
|
||||||
int start, end;
|
int start, end;
|
||||||
|
@ -212,15 +290,19 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_log.Warn("Malformed Range header: " + range);
|
m_log.Warn("[GETTEXTURE]: Malformed Range header: " + range);
|
||||||
response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest;
|
response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else // JP2's or other formats
|
||||||
{
|
{
|
||||||
// Full content request
|
// Full content request
|
||||||
|
response.StatusCode = (int)System.Net.HttpStatusCode.OK;
|
||||||
response.ContentLength = texture.Data.Length;
|
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);
|
response.Body.Write(texture.Data, 0, texture.Data.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,5 +322,83 @@ namespace OpenSim.Region.CoreModules.Avatar.ObjectCaps
|
||||||
start = end = 0;
|
start = end = 0;
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue