// "Therefore those skilled at the unorthodox // are infinite as heaven and earth, // inexhaustible as the great rivers. // When they come to an end, // they begin again, // like the days and months; // they die and are reborn, // like the four seasons." // // - Sun Tsu, // "The Art of War" using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Net; using System.Text; using System.Threading; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.Core.Handlers { /// /// Handler for all loading image logic.
///

/// Loading by .
/// Loading by file path.
/// Loading by URI.
///

///
/// /// /// Supports sync and async image loading. /// /// /// If the image object is created by the handler on calling dispose of the handler the image will be released, this /// makes release of unused images faster as they can be large.
/// Disposing image load handler will also cancel download of image from the web. ///
///
internal sealed class ImageLoadHandler : IDisposable { #region Fields and Consts /// /// the container of the html to handle load image for /// private readonly HtmlContainerInt _htmlContainer; /// /// callback raised when image load process is complete with image or without /// private readonly ActionInt _loadCompleteCallback; /// /// the web client used to download image from URL (to cancel on dispose) /// private WebClient _client; /// /// Must be open as long as the image is in use /// private FileStream _imageFileStream; /// /// the image instance of the loaded image /// private RImage _image; /// /// the image rectangle restriction as returned from image load event /// private RRect _imageRectangle; /// /// to know if image load event callback was sync or async raised /// private bool _asyncCallback; /// /// flag to indicate if to release the image object on box dispose (only if image was loaded by the box) /// private bool _releaseImageObject; /// /// is the handler has been disposed /// private bool _disposed; #endregion /// /// Init. /// /// the container of the html to handle load image for /// callback raised when image load process is complete with image or without public ImageLoadHandler(HtmlContainerInt htmlContainer, ActionInt loadCompleteCallback) { ArgChecker.AssertArgNotNull(htmlContainer, "htmlContainer"); ArgChecker.AssertArgNotNull(loadCompleteCallback, "loadCompleteCallback"); _htmlContainer = htmlContainer; _loadCompleteCallback = loadCompleteCallback; } /// /// the image instance of the loaded image /// public RImage Image { get { return _image; } } /// /// the image rectangle restriction as returned from image load event /// public RRect Rectangle { get { return _imageRectangle; } } /// /// Set image of this image box by analyzing the src attribute.
/// Load the image from inline base64 encoded string.
/// Or from calling property/method on the bridge object that returns image or URL to image.
/// Or from file path
/// Or from URI. ///
/// /// File path and URI image loading is executed async and after finishing calling /// on the main thread and not thread-pool. /// /// the source of the image to load /// the collection of attributes on the element to use in event /// the image object (null if failed) public void LoadImage(string src, Dictionary attributes) { try { var args = new HtmlImageLoadEventArgs(src, attributes, OnHtmlImageLoadEventCallback); _htmlContainer.RaiseHtmlImageLoadEvent(args); _asyncCallback = !_htmlContainer.AvoidAsyncImagesLoading; if (!args.Handled) { if (!string.IsNullOrEmpty(src)) { if (src.StartsWith("data:image", StringComparison.CurrentCultureIgnoreCase)) { SetFromInlineData(src); } else { SetImageFromPath(src); } } else { ImageLoadComplete(false); } } } catch (Exception ex) { _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Exception in handling image source", ex); ImageLoadComplete(false); } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { _disposed = true; ReleaseObjects(); } #region Private methods /// /// Set the image using callback from load image event, use the given data. /// /// the path to the image to load (file path or uri) /// the image to load /// optional: limit to specific rectangle of the image and not all of it private void OnHtmlImageLoadEventCallback(string path, object image, RRect imageRectangle) { if (!_disposed) { _imageRectangle = imageRectangle; if (image != null) { _image = _htmlContainer.Adapter.ConvertImage(image); ImageLoadComplete(_asyncCallback); } else if (!string.IsNullOrEmpty(path)) { SetImageFromPath(path); } else { ImageLoadComplete(_asyncCallback); } } } /// /// Load the image from inline base64 encoded string data. /// /// the source that has the base64 encoded image private void SetFromInlineData(string src) { _image = GetImageFromData(src); if (_image == null) _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed extract image from inline data"); _releaseImageObject = true; ImageLoadComplete(false); } /// /// Extract image object from inline base64 encoded data in the src of the html img element. /// /// the source that has the base64 encoded image /// image from base64 data string or null if failed private RImage GetImageFromData(string src) { var s = src.Substring(src.IndexOf(':') + 1).Split(new[] { ',' }, 2); if (s.Length == 2) { int imagePartsCount = 0, base64PartsCount = 0; foreach (var part in s[0].Split(new[] { ';' })) { var pPart = part.Trim(); if (pPart.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) imagePartsCount++; if (pPart.Equals("base64", StringComparison.InvariantCultureIgnoreCase)) base64PartsCount++; } if (imagePartsCount > 0) { byte[] imageData = base64PartsCount > 0 ? Convert.FromBase64String(s[1].Trim()) : new UTF8Encoding().GetBytes(Uri.UnescapeDataString(s[1].Trim())); return _htmlContainer.Adapter.ImageFromStream(new MemoryStream(imageData)); } } return null; } /// /// Load image from path of image file or URL. /// /// the file path or uri to load image from private void SetImageFromPath(string path) { var uri = CommonUtils.TryGetUri(path); if (uri != null && uri.Scheme != "file") { SetImageFromUrl(uri); } else { var fileInfo = CommonUtils.TryGetFileInfo(uri != null ? uri.AbsolutePath : path); if (fileInfo != null) { SetImageFromFile(fileInfo); } else { _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed load image, invalid source: " + path); ImageLoadComplete(false); } } } /// /// Load the image file on thread-pool thread and calling after. /// /// the file path to get the image from private void SetImageFromFile(FileInfo source) { if (source.Exists) { if (_htmlContainer.AvoidAsyncImagesLoading) LoadImageFromFile(source); else ThreadPool.QueueUserWorkItem(state => LoadImageFromFile(source)); } else { ImageLoadComplete(); } } /// /// Load the image file on thread-pool thread and calling after.
/// Calling on the main thread and not thread-pool. ///
/// the file path to get the image from private void LoadImageFromFile(FileInfo source) { try { if (source.Exists) { _imageFileStream = File.Open(source.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); _image = _htmlContainer.Adapter.ImageFromStream(_imageFileStream); _releaseImageObject = true; } ImageLoadComplete(); } catch (Exception ex) { _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed to load image from disk: " + source, ex); ImageLoadComplete(); } } /// /// Load image from the given URI by downloading it.
/// Create local file name in temp folder from the URI, if the file already exists use it as it has already been downloaded. /// If not download the file using . ///
private void SetImageFromUrl(Uri source) { var filePath = CommonUtils.GetLocalfileName(source); if (filePath.Exists && filePath.Length > 0) { SetImageFromFile(filePath); } else { if (_htmlContainer.AvoidAsyncImagesLoading) DownloadImageFromUrl(source, filePath); else ThreadPool.QueueUserWorkItem(DownloadImageFromUrlAsync, new KeyValuePair(source, filePath)); } } /// /// Download the requested file in the URI to the given file path.
/// Use async sockets API to download from web, . ///
private void DownloadImageFromUrl(Uri source, FileInfo filePath) { try { using (var client = _client = new WebClient()) { client.DownloadFile(source, filePath.FullName); OnDownloadImageCompleted(false, null, filePath, client); } } catch (Exception ex) { OnDownloadImageCompleted(false, ex, null, null); } } /// /// Download the requested file in the URI to the given file path.
/// Use async sockets API to download from web, . ///
/// key value pair of URL and file info to download the file to private void DownloadImageFromUrlAsync(object data) { var uri = ((KeyValuePair)data).Key; var filePath = ((KeyValuePair)data).Value; try { _client = new WebClient(); _client.DownloadFileCompleted += OnDownloadImageCompleted; _client.DownloadFileAsync(uri, filePath.FullName, filePath); } catch (Exception ex) { OnDownloadImageCompleted(false, ex, null, null); } } /// /// On download image complete to local file use to load the image file.
/// If the download canceled do nothing, if failed report error. ///
private void OnDownloadImageCompleted(object sender, AsyncCompletedEventArgs e) { try { using (var client = (WebClient)sender) { client.DownloadFileCompleted -= OnDownloadImageCompleted; OnDownloadImageCompleted(e.Cancelled, e.Error, (FileInfo)e.UserState, client); } } catch (Exception ex) { OnDownloadImageCompleted(false, ex, null, null); } } /// /// On download image complete to local file use to load the image file.
/// If the download canceled do nothing, if failed report error. ///
private void OnDownloadImageCompleted(bool cancelled, Exception error, FileInfo filePath, WebClient client) { if (!cancelled && !_disposed) { if (error == null) { filePath.Refresh(); var contentType = CommonUtils.GetResponseContentType(client); if (contentType != null && contentType.StartsWith("image", StringComparison.OrdinalIgnoreCase)) { LoadImageFromFile(filePath); } else { _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed to load image, not image content type: " + contentType); ImageLoadComplete(); filePath.Delete(); } } else { _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed to load image from URL: " + client.BaseAddress, error); ImageLoadComplete(); } } } /// /// Flag image load complete and request refresh for re-layout and invalidate. /// private void ImageLoadComplete(bool async = true) { // can happen if some operation return after the handler was disposed if (_disposed) ReleaseObjects(); else _loadCompleteCallback(_image, _imageRectangle, async); } /// /// Release the image and client objects. /// private void ReleaseObjects() { if (_releaseImageObject && _image != null) { _image.Dispose(); _image = null; } if (_imageFileStream != null) { _imageFileStream.Dispose(); _imageFileStream = null; } if (_client != null) { _client.CancelAsync(); _client.Dispose(); _client = null; } } #endregion } }