// "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.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.Handlers; using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.Core.Dom { /// /// CSS box for iframe element.
/// If the iframe is of embedded YouTube or Vimeo video it will show image with play. ///
internal sealed class CssBoxFrame : CssBox { #region Fields and Consts /// /// the image word of this image box /// private readonly CssRectImage _imageWord; /// /// is the iframe is of embeded video /// private readonly bool _isVideo; /// /// the title of the video /// private string _videoTitle; /// /// the url of the video thumbnail image /// private string _videoImageUrl; /// /// link to the video on the site /// private string _videoLinkUrl; /// /// handler used for image loading by source /// private ImageLoadHandler _imageLoadHandler; /// /// is image load is finished, used to know if no image is found /// private bool _imageLoadingComplete; #endregion /// /// Init. /// /// the parent box of this box /// the html tag data of this box public CssBoxFrame(CssBox parent, HtmlTag tag) : base(parent, tag) { _imageWord = new CssRectImage(this); Words.Add(_imageWord); Uri uri; if (Uri.TryCreate(GetAttribute("src"), UriKind.Absolute, out uri)) { if (uri.Host.IndexOf("youtube.com", StringComparison.InvariantCultureIgnoreCase) > -1) { _isVideo = true; LoadYoutubeDataAsync(uri); } else if (uri.Host.IndexOf("vimeo.com", StringComparison.InvariantCultureIgnoreCase) > -1) { _isVideo = true; LoadVimeoDataAsync(uri); } } if (!_isVideo) { SetErrorBorder(); } } /// /// Is the css box clickable ("a" element is clickable) /// public override bool IsClickable { get { return true; } } /// /// Get the href link of the box (by default get "href" attribute) /// public override string HrefLink { get { return _videoLinkUrl ?? GetAttribute("src"); } } /// /// is the iframe is of embeded video /// public bool IsVideo { get { return _isVideo; } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public override void Dispose() { if (_imageLoadHandler != null) _imageLoadHandler.Dispose(); base.Dispose(); } #region Private methods /// /// Load YouTube video data (title, image, link) by calling YouTube API. /// private void LoadYoutubeDataAsync(Uri uri) { ThreadPool.QueueUserWorkItem(state => { try { var apiUri = new Uri(string.Format("http://gdata.youtube.com/feeds/api/videos/{0}?v=2&alt=json", uri.Segments[2])); var client = new WebClient(); client.Encoding = Encoding.UTF8; client.DownloadStringCompleted += OnDownloadYoutubeApiCompleted; client.DownloadStringAsync(apiUri); } catch (Exception ex) { HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to get youtube video data: " + uri, ex); HtmlContainer.RequestRefresh(false); } }); } /// /// Parse YouTube API response to get video data (title, image, link). /// private void OnDownloadYoutubeApiCompleted(object sender, DownloadStringCompletedEventArgs e) { try { if (!e.Cancelled) { if (e.Error == null) { var idx = e.Result.IndexOf("\"media$title\"", StringComparison.Ordinal); if (idx > -1) { idx = e.Result.IndexOf("\"$t\"", idx); if (idx > -1) { idx = e.Result.IndexOf('"', idx + 4); if (idx > -1) { var endIdx = e.Result.IndexOf('"', idx + 1); while (e.Result[endIdx - 1] == '\\') endIdx = e.Result.IndexOf('"', endIdx + 1); if (endIdx > -1) { _videoTitle = e.Result.Substring(idx + 1, endIdx - idx - 1).Replace("\\\"", "\""); } } } } idx = e.Result.IndexOf("\"media$thumbnail\"", StringComparison.Ordinal); if (idx > -1) { var iidx = e.Result.IndexOf("sddefault", idx); if (iidx > -1) { if (string.IsNullOrEmpty(Width)) Width = "640px"; if (string.IsNullOrEmpty(Height)) Height = "480px"; } else { iidx = e.Result.IndexOf("hqdefault", idx); if (iidx > -1) { if (string.IsNullOrEmpty(Width)) Width = "480px"; if (string.IsNullOrEmpty(Height)) Height = "360px"; } else { iidx = e.Result.IndexOf("mqdefault", idx); if (iidx > -1) { if (string.IsNullOrEmpty(Width)) Width = "320px"; if (string.IsNullOrEmpty(Height)) Height = "180px"; } else { iidx = e.Result.IndexOf("default", idx); if (string.IsNullOrEmpty(Width)) Width = "120px"; if (string.IsNullOrEmpty(Height)) Height = "90px"; } } } iidx = e.Result.LastIndexOf("http:", iidx, StringComparison.Ordinal); if (iidx > -1) { var endIdx = e.Result.IndexOf('"', iidx); if (endIdx > -1) { _videoImageUrl = e.Result.Substring(iidx, endIdx - iidx).Replace("\\\"", "\"").Replace("\\", ""); } } } idx = e.Result.IndexOf("\"link\"", StringComparison.Ordinal); if (idx > -1) { idx = e.Result.IndexOf("http:", idx); if (idx > -1) { var endIdx = e.Result.IndexOf('"', idx); if (endIdx > -1) { _videoLinkUrl = e.Result.Substring(idx, endIdx - idx).Replace("\\\"", "\"").Replace("\\", ""); } } } } else { HandleDataLoadFailure(e.Error, "YouTube"); } } } catch (Exception ex) { HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to parse YouTube video response", ex); } HandlePostApiCall(sender); } /// /// Load Vimeo video data (title, image, link) by calling Vimeo API. /// private void LoadVimeoDataAsync(Uri uri) { ThreadPool.QueueUserWorkItem(state => { try { var apiUri = new Uri(string.Format("http://vimeo.com/api/v2/video/{0}.json", uri.Segments[2])); var client = new WebClient(); client.Encoding = Encoding.UTF8; client.DownloadStringCompleted += OnDownloadVimeoApiCompleted; client.DownloadStringAsync(apiUri); } catch (Exception ex) { _imageLoadingComplete = true; SetErrorBorder(); HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to get vimeo video data: " + uri, ex); HtmlContainer.RequestRefresh(false); } }); } /// /// Parse Vimeo API response to get video data (title, image, link). /// private void OnDownloadVimeoApiCompleted(object sender, DownloadStringCompletedEventArgs e) { try { if (!e.Cancelled) { if (e.Error == null) { var idx = e.Result.IndexOf("\"title\"", StringComparison.Ordinal); if (idx > -1) { idx = e.Result.IndexOf('"', idx + 7); if (idx > -1) { var endIdx = e.Result.IndexOf('"', idx + 1); while (e.Result[endIdx - 1] == '\\') endIdx = e.Result.IndexOf('"', endIdx + 1); if (endIdx > -1) { _videoTitle = e.Result.Substring(idx + 1, endIdx - idx - 1).Replace("\\\"", "\""); } } } idx = e.Result.IndexOf("\"thumbnail_large\"", StringComparison.Ordinal); if (idx > -1) { if (string.IsNullOrEmpty(Width)) Width = "640"; if (string.IsNullOrEmpty(Height)) Height = "360"; } else { idx = e.Result.IndexOf("thumbnail_medium", idx); if (idx > -1) { if (string.IsNullOrEmpty(Width)) Width = "200"; if (string.IsNullOrEmpty(Height)) Height = "150"; } else { idx = e.Result.IndexOf("thumbnail_small", idx); if (string.IsNullOrEmpty(Width)) Width = "100"; if (string.IsNullOrEmpty(Height)) Height = "75"; } } if (idx > -1) { idx = e.Result.IndexOf("http:", idx); if (idx > -1) { var endIdx = e.Result.IndexOf('"', idx); if (endIdx > -1) { _videoImageUrl = e.Result.Substring(idx, endIdx - idx).Replace("\\\"", "\"").Replace("\\", ""); } } } idx = e.Result.IndexOf("\"url\"", StringComparison.Ordinal); if (idx > -1) { idx = e.Result.IndexOf("http:", idx); if (idx > -1) { var endIdx = e.Result.IndexOf('"', idx); if (endIdx > -1) { _videoLinkUrl = e.Result.Substring(idx, endIdx - idx).Replace("\\\"", "\"").Replace("\\", ""); } } } } else { HandleDataLoadFailure(e.Error, "Vimeo"); } } } catch (Exception ex) { HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to parse Vimeo video response", ex); } HandlePostApiCall(sender); } /// /// Handle error occurred during video data load to handle if the video was not found. /// /// the exception that occurred during data load web request /// the name of the video source (YouTube/Vimeo/Etc.) private void HandleDataLoadFailure(Exception ex, string source) { var webError = ex as WebException; var webResponse = webError != null ? webError.Response as HttpWebResponse : null; if (webResponse != null && webResponse.StatusCode == HttpStatusCode.NotFound) { _videoTitle = "The video is not found, possibly removed by the user."; } else { HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to load " + source + " video data", ex); } } /// /// Create image handler for downloading video image if found and release the WebClient instance used for API call. /// private void HandlePostApiCall(object sender) { try { if (_videoImageUrl != null) { _imageLoadHandler = new ImageLoadHandler(HtmlContainer, OnLoadImageComplete); _imageLoadHandler.LoadImage(_videoImageUrl, HtmlTag != null ? HtmlTag.Attributes : null); } else { _imageLoadingComplete = true; SetErrorBorder(); } var webClient = (WebClient)sender; webClient.DownloadStringCompleted -= OnDownloadYoutubeApiCompleted; webClient.DownloadStringCompleted -= OnDownloadVimeoApiCompleted; webClient.Dispose(); HtmlContainer.RequestRefresh(IsLayoutRequired()); } catch { } } /// /// Paints the fragment /// /// the device to draw to protected override void PaintImp(RGraphics g) { var rects = CommonUtils.GetFirstValueOrDefault(Rectangles); RPoint offset = HtmlContainer != null ? HtmlContainer.ScrollOffset : RPoint.Empty; rects.Offset(offset); var clipped = RenderUtils.ClipGraphicsByOverflow(g, this); PaintBackground(g, rects, true, true); BordersDrawHandler.DrawBoxBorders(g, this, rects, true, true); var word = Words[0]; var tmpRect = word.Rectangle; tmpRect.Offset(offset); tmpRect.Height -= ActualBorderTopWidth + ActualBorderBottomWidth + ActualPaddingTop + ActualPaddingBottom; tmpRect.Y += ActualBorderTopWidth + ActualPaddingTop; tmpRect.X = Math.Floor(tmpRect.X); tmpRect.Y = Math.Floor(tmpRect.Y); var rect = tmpRect; DrawImage(g, offset, rect); DrawTitle(g, rect); DrawPlay(g, rect); if (clipped) g.PopClip(); } /// /// Draw video image over the iframe if found. /// private void DrawImage(RGraphics g, RPoint offset, RRect rect) { if (_imageWord.Image != null) { if (rect.Width > 0 && rect.Height > 0) { if (_imageWord.ImageRectangle == RRect.Empty) g.DrawImage(_imageWord.Image, rect); else g.DrawImage(_imageWord.Image, rect, _imageWord.ImageRectangle); if (_imageWord.Selected) { g.DrawRectangle(GetSelectionBackBrush(g, true), _imageWord.Left + offset.X, _imageWord.Top + offset.Y, _imageWord.Width + 2, DomUtils.GetCssLineBoxByWord(_imageWord).LineHeight); } } } else if (_isVideo && !_imageLoadingComplete) { RenderUtils.DrawImageLoadingIcon(g, HtmlContainer, rect); if (rect.Width > 19 && rect.Height > 19) { g.DrawRectangle(g.GetPen(RColor.LightGray), rect.X, rect.Y, rect.Width, rect.Height); } } } /// /// Draw video title on top of the iframe if found. /// private void DrawTitle(RGraphics g, RRect rect) { if (_videoTitle != null && _imageWord.Width > 40 && _imageWord.Height > 40) { var font = HtmlContainer.Adapter.GetFont("Arial", 9f, RFontStyle.Regular); g.DrawRectangle(g.GetSolidBrush(RColor.FromArgb(160, 0, 0, 0)), rect.Left, rect.Top, rect.Width, ActualFont.Height + 7); var titleRect = new RRect(rect.Left + 3, rect.Top + 3, rect.Width - 6, rect.Height - 6); g.DrawString(_videoTitle, font, RColor.WhiteSmoke, titleRect.Location, RSize.Empty, false); } } /// /// Draw play over the iframe if we found link url. /// private void DrawPlay(RGraphics g, RRect rect) { if (_isVideo && _imageWord.Width > 70 && _imageWord.Height > 50) { var prevMode = g.SetAntiAliasSmoothingMode(); var size = new RSize(60, 40); var left = rect.Left + (rect.Width - size.Width) / 2; var top = rect.Top + (rect.Height - size.Height) / 2; g.DrawRectangle(g.GetSolidBrush(RColor.FromArgb(160, 0, 0, 0)), left, top, size.Width, size.Height); RPoint[] points = { new RPoint(left + size.Width / 3f + 1,top + 3 * size.Height / 4f), new RPoint(left + size.Width / 3f + 1, top + size.Height / 4f), new RPoint(left + 2 * size.Width / 3f + 1, top + size.Height / 2f) }; g.DrawPolygon(g.GetSolidBrush(RColor.White), points); g.ReturnPreviousSmoothingMode(prevMode); } } /// /// Assigns words its width and height /// /// the device to use internal override void MeasureWordsSize(RGraphics g) { if (!_wordsSizeMeasured) { MeasureWordSpacing(g); _wordsSizeMeasured = true; } CssLayoutEngine.MeasureImageSize(_imageWord); } /// /// Set error image border on the image box. /// private void SetErrorBorder() { SetAllBorders(CssConstants.Solid, "2px", "#A0A0A0"); BorderRightColor = BorderBottomColor = "#E3E3E3"; } /// /// On image load process is complete with image or without update the image box. /// /// the image loaded or null if failed /// the source rectangle to draw in the image (empty - draw everything) /// is the callback was called async to load image call private void OnLoadImageComplete(RImage image, RRect rectangle, bool async) { _imageWord.Image = image; _imageWord.ImageRectangle = rectangle; _imageLoadingComplete = true; _wordsSizeMeasured = false; if (_imageLoadingComplete && image == null) { SetErrorBorder(); } if (async) { HtmlContainer.RequestRefresh(IsLayoutRequired()); } } private bool IsLayoutRequired() { var width = new CssLength(Width); var height = new CssLength(Height); return (width.Number <= 0 || width.Unit != CssUnit.Pixels) || (height.Number <= 0 || height.Unit != CssUnit.Pixels); } #endregion } }