/**
* FileReference.cs
*
* Copyright 2009, Moxiecode Systems AB
* Released under GPL License.
*
* License: http://www.plupload.com/license
* Contributing: http://www.plupload.com/contributing
*/
using System;
using System.IO;
using System.Threading;
using System.Windows.Threading;
using System.Net;
using System.Text.RegularExpressions;
using System.Windows.Browser;
using System.Windows.Media.Imaging;
using System.Collections.Generic;
using FluxJpeg.Core.Encoder;
using FluxJpeg.Core;
using Plupload.PngEncoder;
namespace Moxiecode.Plupload {
enum ImageType {
Jpeg,
Png
}
///
/// Description of File.
///
public class FileReference {
#region private fields
private string name, uploadUrl, id, targetName, mimeType;
private FileInfo info;
private SynchronizationContext syncContext;
private int chunks, chunkSize;
private bool multipart, chunking;
private long size, chunk;
private string fileDataName;
private Dictionary multipartParams;
private Dictionary headers;
private Stream fileStream;
private Stream imageStream;
private HttpWebRequest req;
#endregion
/// Upload complete delegate.
public delegate void UploadCompleteHandler(object sender, UploadEventArgs e);
/// Upload chunk compleate delegate.
public delegate void UploadChunkCompleteHandler(object sender, UploadEventArgs e);
/// Upload error delegate.
public delegate void ErrorHandler(object sender, ErrorEventArgs e);
/// Upload progress delegate.
public delegate void ProgressHandler(object sender, ProgressEventArgs e);
/// Upload complete event
public event UploadCompleteHandler UploadComplete;
/// Upload chunk complete event
public event UploadChunkCompleteHandler UploadChunkComplete;
/// Error event
public event ErrorHandler Error;
/// Progress event
public event ProgressHandler Progress;
///
/// Main constructor for the file reference.
///
/// Unique file id for item.
/// FileInfo that got returned from a file selection.
public FileReference(string id, FileInfo info) {
this.id = id;
this.name = info.Name;
this.info = info;
this.size = info.Length;
}
/// Unique id for the file reference.
public string Id {
get { return id; }
}
/// File name to use with upload.
public string Name {
get { return name; }
set { name = value; }
}
/// File size for the selected file.
public long Size {
get { return this.size; }
}
///
/// Uploads the file to the specific url and using the specified chunk_size.
///
/// URL to upload to.
/// Chunk size to use.
/// Image width to scale to.
/// Image height to scale to.
/// Image quality to store as.
public void Upload(string upload_url, string json_settings) {
int chunkSize = 0, imageWidth = 0, imageHeight = 0, imageQuality = 90;
Dictionary settings = (Dictionary) Moxiecode.Plupload.Utils.JsonReader.ParseJson(json_settings);
chunkSize = Convert.ToInt32(settings["chunk_size"]);
imageWidth = Convert.ToInt32(settings["image_width"]);
imageHeight = Convert.ToInt32(settings["image_height"]);
imageQuality = Convert.ToInt32(settings["image_quality"]);
this.fileDataName = (string)settings["file_data_name"];
this.multipart = Convert.ToBoolean(settings["multipart"]);
this.multipartParams = (Dictionary)settings["multipart_params"];
this.headers = (Dictionary)settings["headers"];
this.targetName = (string) settings["name"];
this.mimeType = (string) settings["mime"];
this.chunk = 0;
this.chunking = chunkSize > 0;
this.uploadUrl = upload_url;
try {
// Is jpeg and image size is defined
if (Regex.IsMatch(this.name, @"\.(jpeg|jpg|png)$", RegexOptions.IgnoreCase) && (imageWidth != 0 || imageHeight != 0 || imageQuality != 0))
{
if (Regex.IsMatch(this.name, @"\.png$"))
this.imageStream = this.ResizeImage(this.info.OpenRead(), imageWidth, imageHeight, imageQuality, ImageType.Png);
else
this.imageStream = this.ResizeImage(this.info.OpenRead(), imageWidth, imageHeight, imageQuality, ImageType.Jpeg);
this.imageStream.Seek(0, SeekOrigin.Begin);
this.size = this.imageStream.Length;
}
} catch (Exception ex) {
syncContext.Send(delegate {
this.OnIOError(new ErrorEventArgs(ex.Message, 0, this.chunks));
}, this);
}
if (this.chunking) {
this.chunkSize = chunkSize;
this.chunks = (int) Math.Ceiling((double) this.Size / (double) chunkSize);
} else {
this.chunkSize = (int) this.Size;
this.chunks = 1;
}
this.UploadNextChunk();
}
private int ReadByteRange(byte[] buffer, long position, int offset, int count) {
int bytes = -1;
// Read from image memory stream if it's defined
if (this.imageStream != null) {
this.imageStream.Seek(position, SeekOrigin.Begin);
return this.imageStream.Read(buffer, offset, count);
}
// Open the file and read the specified part of it
if (this.fileStream == null) {
this.fileStream = this.info.OpenRead();
}
bytes = this.fileStream.Read(buffer, offset, count);
return bytes;
}
///
/// Uploads the next chunk if there are more in queue.
///
/// True/false if there are more chunks to be uploaded.
public bool UploadNextChunk() {
string url = this.uploadUrl;
// Is there more chunks
if (this.chunk >= this.chunks)
return false;
this.syncContext = SynchronizationContext.Current;
// Add name, chunk and chunks to query string when we don't use multipart
if (!this.multipart) {
if (url.IndexOf('?') == -1) {
url += '?';
} else {
url += '&';
}
url += "name=" + Uri.EscapeDataString(this.targetName);
if (this.chunking) {
url += "&chunk=" + this.chunk;
url += "&chunks=" + this.chunks;
}
}
this.req = WebRequest.Create(new Uri(HtmlPage.Document.DocumentUri, url)) as HttpWebRequest;
this.req.Method = "POST";
// Add custom headers
if (this.headers != null) {
foreach (string key in this.headers.Keys) {
if (this.headers[key] == null)
continue;
switch (key.ToLower())
{
// in silverlight 3, these are set by the web browser that hosts the Silverlight application.
// http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest%28v=vs.95%29.aspx
case "connection":
case "content-length":
case "expect":
case "if-modified-since":
case "referer":
case "transfer-encoding":
case "user-agent":
break;
// in silverlight this isn't supported, can not find reference to why not
case "range":
break;
// in .NET Framework 3.5 and below, these are set by the system.
// http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest%28v=VS.90%29.aspx
case "date":
case "host":
break;
case "accept":
this.req.Accept = (string)this.headers[key];
break;
case "content-type":
this.req.ContentType = (string)this.headers[key];
break;
default:
this.req.Headers[key] = (string)this.headers[key];
break;
}
}
}
IAsyncResult asyncResult = this.req.BeginGetRequestStream(new AsyncCallback(RequestStreamCallback), this.req);
return true;
}
///
/// Cancels uploading the current file.
///
public void CancelUpload() {
if (this.req != null) {
this.req.Abort();
this.req = null;
DisposeStreams();
}
}
#region protected methods
protected virtual void OnUploadComplete(UploadEventArgs e) {
DisposeStreams();
if (UploadComplete != null)
UploadComplete(this, e);
}
protected virtual void OnUploadChunkComplete(UploadEventArgs e) {
if (UploadChunkComplete != null)
UploadChunkComplete(this, e);
}
protected virtual void OnIOError(ErrorEventArgs e) {
DisposeStreams();
if (Error != null)
Error(this, e);
}
protected virtual void OnProgress(ProgressEventArgs e) {
if (Progress != null)
Progress(this, e);
}
#endregion
#region private methods
private void RequestStreamCallback(IAsyncResult ar) {
HttpWebRequest request = (HttpWebRequest) ar.AsyncState;
string boundary = "----pluploadboundary" + DateTime.Now.Ticks, dashdash = "--", crlf = "\r\n";
Stream requestStream = null;
byte[] buffer = new byte[1048576], strBuff;
int bytes;
long loaded = 0, end = 0;
int percent, lastPercent = 0;
try {
requestStream = request.EndGetRequestStream(ar);
if (this.multipart) {
request.ContentType = "multipart/form-data; boundary=" + boundary;
// Add name to multipart array
this.multipartParams["name"] = this.targetName;
// Add chunking when needed
if (this.chunking) {
this.multipartParams["chunk"] = this.chunk;
this.multipartParams["chunks"] = this.chunks;
}
// Append mutlipart parameters
foreach (KeyValuePair pair in this.multipartParams) {
strBuff = this.StrToByteArray(dashdash + boundary + crlf +
"Content-Disposition: form-data; name=\"" + pair.Key + '"' + crlf + crlf +
pair.Value + crlf
);
requestStream.Write(strBuff, 0, strBuff.Length);
}
// Append multipart file header
strBuff = this.StrToByteArray(
dashdash + boundary + crlf +
"Content-Disposition: form-data; name=\"" + this.fileDataName + "\"; filename=\"" + this.name + '"' +
crlf + "Content-Type: " + this.mimeType + crlf + crlf
);
requestStream.Write(strBuff, 0, strBuff.Length);
} else {
request.ContentType = "application/octet-stream";
}
// Move to start
loaded = this.chunk * this.chunkSize;
// Find end
end = (this.chunk + 1) * this.chunkSize;
if (end > this.Size)
end = this.Size;
while (loaded < end && (bytes = ReadByteRange(buffer, loaded, 0, (int)(end - loaded < buffer.Length ? end - loaded : buffer.Length))) != 0) {
loaded += bytes;
percent = (int) Math.Round((double) loaded / (double) this.Size * 100.0);
if (percent > lastPercent) {
syncContext.Post(delegate {
if (percent > lastPercent) {
this.OnProgress(new ProgressEventArgs(loaded, this.Size));
lastPercent = percent;
}
}, this);
}
requestStream.Write(buffer, 0, bytes);
requestStream.Flush();
}
// Append multipart file footer
if (this.multipart) {
strBuff = this.StrToByteArray(crlf + dashdash + boundary + dashdash + crlf);
requestStream.Write(strBuff, 0, strBuff.Length);
}
} catch (Exception ex) {
syncContext.Send(delegate {
this.OnIOError(new ErrorEventArgs(ex.Message, this.chunk, this.chunks));
}, this);
} finally {
try {
if (requestStream != null) {
requestStream.Close();
requestStream.Dispose();
requestStream = null;
}
} catch (Exception ex) {
syncContext.Send(delegate {
this.OnIOError(new ErrorEventArgs(ex.Message, this.chunk, this.chunks));
}, this);
}
}
try {
request.BeginGetResponse(new AsyncCallback(ResponseCallback), request);
}
catch (WebException ex)
{
if (ex.Status != WebExceptionStatus.RequestCanceled) {
syncContext.Send(delegate {
this.OnIOError(new ErrorEventArgs(ex.Message, this.chunk, this.chunks));
}, this);
}
}
catch (Exception ex) {
syncContext.Send(delegate {
this.OnIOError(new ErrorEventArgs(ex.Message, this.chunk, this.chunks));
}, this);
}
}
private void ResponseCallback(IAsyncResult ar) {
try
{
HttpWebRequest request = ar.AsyncState as HttpWebRequest;
WebResponse response = request.EndGetResponse(ar);
syncContext.Post(ExtractResponse, response);
}
catch (WebException ex) {
if (ex.Status != WebExceptionStatus.RequestCanceled) {
syncContext.Send(delegate {
this.OnIOError(new ErrorEventArgs(ex.Message, this.chunk, this.chunks));
}, this);
}
}
catch (Exception ex) {
syncContext.Send(delegate {
this.OnIOError(new ErrorEventArgs(ex.Message, this.chunk, this.chunks));
}, this);
}
}
private void ExtractResponse(object state) {
HttpWebResponse response = state as HttpWebResponse;
StreamReader respReader = null;
Stream respStream = null;
string content;
try {
respStream = response.GetResponseStream();
if (response.StatusCode == HttpStatusCode.OK) {
respReader = new StreamReader(respStream);
if (respStream != null) {
content = respReader.ReadToEnd();
} else
throw new Exception("Error could not open response stream.");
} else
throw new Exception("Error server returned status: " + ((int) response.StatusCode) + " " + response.StatusDescription);
this.chunk++;
syncContext.Send(delegate {
this.OnUploadChunkComplete(new UploadEventArgs(content, this.chunk - 1, this.chunks));
}, this);
} catch (Exception ex) {
syncContext.Send(delegate {
this.OnIOError(new ErrorEventArgs(ex.Message, chunk, chunks));
}, this);
} finally {
if (respStream != null)
respStream.Close();
if (respReader != null)
respReader.Close();
response.Close();
}
}
private void DisposeStreams() {
if (fileStream != null) {
fileStream.Dispose();
fileStream = null;
}
if (imageStream != null) {
imageStream.Dispose();
imageStream = null;
}
}
private void Debug(string msg) {
((ScriptObject) HtmlPage.Window.Eval("console")).Invoke("log", new string[] {msg});
}
private Stream ResizeImage(Stream image_stream, int width, int height, int quality, ImageType type) {
try {
// Load the image as a writeablebitmap
WriteableBitmap writableBitmap;
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.SetSource(image_stream);
writableBitmap = new WriteableBitmap(bitmapImage);
if (width == 0) {
width = writableBitmap.PixelWidth;
}
if (height == 0) {
height = writableBitmap.PixelHeight;
}
double scale = Math.Min((double) width / writableBitmap.PixelWidth, (double) height / writableBitmap.PixelHeight);
// No resize needed
if (scale >= 1.0 && (quality == 0 || type != ImageType.Jpeg))
return image_stream;
if (quality == 0) {
quality = 90;
}
// Setup shorter names and pixelbuffers
int w = writableBitmap.PixelWidth;
int h = writableBitmap.PixelHeight;
int[] p = writableBitmap.Pixels;
byte[][,] imageRaster = new byte[3][,]; // RGB colors
imageRaster[0] = new byte[w, h];
imageRaster[1] = new byte[w, h];
imageRaster[2] = new byte[w, h];
// Copy WriteableBitmap data into buffer for FluxJpeg
int i = 0;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int color = p[i++];
imageRaster[0][x, y] = (byte) (color >> 16); // R
imageRaster[1][x, y] = (byte) (color >> 8); // G
imageRaster[2][x, y] = (byte) (color); // B
}
}
// Create new FluxJpeg image based on pixel data
Image jpegImage = new Image(new ColorModel {
colorspace = ColorSpace.RGB
}, imageRaster);
ImageResizer resizer = new ImageResizer(jpegImage);
Image resizedImage;
if (scale < 1.0) {
// Calc new proportional size
width = (int) Math.Round(writableBitmap.PixelWidth * scale);
height = (int) Math.Round(writableBitmap.PixelHeight * scale);
// Resize the image
resizedImage = resizer.Resize(width, height, FluxJpeg.Core.Filtering.ResamplingFilters.LowpassAntiAlias);
} else {
resizedImage = jpegImage;
}
Stream imageStream = new MemoryStream();
if (type == ImageType.Jpeg) {
// Encode the resized image as Jpeg
JpegEncoder jpegEncoder = new JpegEncoder(resizedImage, quality, imageStream);
jpegEncoder.Encode();
} else {
int[] pixelBuffer = new int[resizedImage.Height * resizedImage.Width];
byte[][,] resizedRaster = resizedImage.Raster;
// Convert FJCore raster to PixelBuffer
for (int y = 0; y < resizedImage.Height; y++) {
for (int x = 0; x < resizedImage.Width; x++) {
int color = 0;
color = color | resizedRaster[0][x, y] << 16; // R
color = color | resizedRaster[1][x, y] << 8; // G
color = color | resizedRaster[2][x, y]; // B
pixelBuffer[(y * resizedImage.Width) + x] = color;
}
}
// Encode the resized image as Png
PngEncoder pngEncoder = new PngEncoder(pixelBuffer, resizedImage.Width, resizedImage.Height, false, PngEncoder.FILTER_NONE, Deflater.BEST_COMPRESSION);
byte[] pngBuffer = pngEncoder.pngEncode();
imageStream.Write(pngBuffer, 0, pngBuffer.Length);
}
return imageStream;
} catch {
// Ignore the error and let the server resize the image
}
return image_stream;
}
private byte[] StrToByteArray(string str) {
System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
return encoding.GetBytes(str);
}
#endregion
}
///
/// Upload event arguments class.
///
public class UploadEventArgs : EventArgs {
#region private fields
private string response;
private long chunk;
private int chunks;
#endregion
///
/// Main constructor for the upload event.
///
/// Response contents as a string.
public UploadEventArgs(string response) : this(response, 0, 0) {
}
///
/// Main constructor for the upload event.
///
/// Response contents as a string.
/// Current chunk number.
/// Total chunks.
public UploadEventArgs(string response, long chunk, int chunks) {
this.response = response;
this.chunk = chunk;
this.chunks = chunks;
}
/// Response from upload request.
public string Response {
get { return response; }
}
/// Chunk number.
public long Chunk {
get { return chunk; }
}
/// Total number of chunks.
public int Chunks {
get { return chunks; }
}
}
///
/// Error event arguments class.
///
public class ErrorEventArgs : EventArgs {
#region private fields
private string message;
private long chunk;
private int chunks;
#endregion
///
/// Main constructor for the error event.
///
/// Error message.
public ErrorEventArgs(string message) : this(message, 0, 0) {
this.message = message;
}
///
/// Main constructor for the error event.
///
/// Error message.
/// Current chunk number.
/// Total chunks.
public ErrorEventArgs(string message, long chunk, int chunks) {
this.message = message;
this.chunk = chunk;
this.chunks = chunks;
}
/// Chunk number.
public long Chunk {
get { return chunk; }
}
/// Total number of chunks.
public int Chunks {
get { return chunks; }
}
/// Error message.
public string Message {
get { return message; }
}
}
///
/// Progress event arguments class.
///
public class ProgressEventArgs : EventArgs {
#region private fields
private long loaded, total;
#endregion
///
/// Main constructor for the progress events args.
///
/// Number of bytes uploaded.
/// Total bytes to upload.
public ProgressEventArgs(long loaded, long total) {
this.loaded = loaded;
this.total = total;
}
/// Total bytes to upload.
public long Total {
get { return total; }
}
/// Number of bytes upload so far.
public long Loaded {
get { return loaded; }
}
}
}