-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* #6986 Created feature Media Processing Html Filter * Fixing HtmlAgilityPack and upgrading FW and package version to match Orchard.Specs * Adapting MediaProcessingHtmlFilter to IHtmlFilter breaking change * Fixing that Orchard.MediaProcessingHtmlFilter should depend on Orchard.MediaProcessing * Code styling * Using regexes instead of HtmlAgilityPack, thanks GHCP * Updating comments and code styling * Code styling * Reworking ProcessContent to use StringBuilder instead of replaces * Fixing that GetAttributeRegex should find attributes with empty value * Code styling * Fixing that detecting the extension works regardless of casing but it still works with Azure Blob Storage (which is case-sensitive) too * Optimizing image tag regex * Caching attribute regexes * Caching attribute values of img tags * Simplifying attribute value cache --------- Co-authored-by: Arjan Noordende <[email protected]>
- Loading branch information
1 parent
5c57a30
commit 18ce08f
Showing
7 changed files
with
297 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
namespace Orchard.MediaProcessing { | ||
public static class Features { | ||
public const string OrchardMediaProcessingHtmlFilter = "Orchard.MediaProcessingHtmlFilter"; | ||
} | ||
} |
204 changes: 204 additions & 0 deletions
204
src/Orchard.Web/Modules/Orchard.MediaProcessing/Filters/MediaProcessingHtmlFilter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
using Orchard.ContentManagement; | ||
using Orchard.Environment.Extensions; | ||
using Orchard.Forms.Services; | ||
using Orchard.Logging; | ||
using Orchard.MediaProcessing.Models; | ||
using Orchard.MediaProcessing.Services; | ||
using Orchard.Services; | ||
|
||
namespace Orchard.MediaProcessing.Filters { | ||
/// <summary> | ||
/// Resizes any images in HTML provided by parts that support IHtmlFilter and sets an alt text if not already supplied. | ||
/// </summary> | ||
[OrchardFeature(Features.OrchardMediaProcessingHtmlFilter)] | ||
public class MediaProcessingHtmlFilter : IHtmlFilter { | ||
|
||
private readonly IWorkContextAccessor _wca; | ||
private readonly IImageProfileManager _profileManager; | ||
|
||
private MediaHtmlFilterSettingsPart _settingsPart; | ||
private static readonly Regex _imageTagRegex = new Regex(@"<img\b[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled); | ||
private static readonly ConcurrentDictionary<string, Regex> _attributeRegexes = new ConcurrentDictionary<string, Regex>(); | ||
private static readonly ConcurrentDictionary<string, string> _attributeValues = new ConcurrentDictionary<string, string>(); | ||
private static readonly Dictionary<string, string> _validExtensions = new Dictionary<string, string> { | ||
{ ".jpeg", "jpg" }, // For example: .jpeg supports compression (quality), format to 'jpg'. | ||
{ ".jpg", "jpg" }, | ||
{ ".png", null } | ||
}; | ||
|
||
public MediaProcessingHtmlFilter(IWorkContextAccessor wca, IImageProfileManager profileManager) { | ||
_profileManager = profileManager; | ||
_wca = wca; | ||
|
||
Logger = NullLogger.Instance; | ||
} | ||
|
||
public ILogger Logger { get; set; } | ||
|
||
public MediaHtmlFilterSettingsPart Settings { | ||
get { | ||
if (_settingsPart == null) { | ||
_settingsPart = _wca.GetContext().CurrentSite.As<MediaHtmlFilterSettingsPart>(); | ||
} | ||
|
||
return _settingsPart; | ||
} | ||
} | ||
|
||
public string ProcessContent(string text, HtmlFilterContext context) { | ||
if (string.IsNullOrWhiteSpace(text) || context.Flavor != "html") { | ||
return text; | ||
} | ||
|
||
var matches = _imageTagRegex.Matches(text); | ||
|
||
if (matches.Count == 0) { | ||
return text; | ||
} | ||
|
||
var offset = 0; // This tracks where last image tag ended in the original HTML. | ||
var newText = new StringBuilder(); | ||
|
||
foreach (Match match in matches) { | ||
newText.Append(text.Substring(offset, match.Index - offset)); | ||
offset = match.Index + match.Length; | ||
var imgTag = match.Value; | ||
var processedImgTag = ProcessImageContent(imgTag); | ||
|
||
if (Settings.PopulateAlt) { | ||
processedImgTag = ProcessImageAltContent(processedImgTag); | ||
} | ||
|
||
newText.Append(processedImgTag); | ||
} | ||
|
||
newText.Append(text.Substring(offset)); | ||
|
||
return newText.ToString(); | ||
} | ||
|
||
private string ProcessImageContent(string imgTag) { | ||
if (imgTag.Contains("noresize")) { | ||
return imgTag; | ||
} | ||
|
||
var src = GetAttributeValue(imgTag, "src"); | ||
var ext = string.IsNullOrEmpty(src) ? null : Path.GetExtension(src).ToLowerInvariant(); | ||
var width = GetAttributeValueInt(imgTag, "width"); | ||
var height = GetAttributeValueInt(imgTag, "height"); | ||
|
||
if (width > 0 && height > 0 | ||
&& !string.IsNullOrEmpty(src) | ||
&& !src.Contains("_Profiles") | ||
&& _validExtensions.ContainsKey(ext)) { | ||
try { | ||
// If the image has a combination of width, height and valid extension, that is not already in | ||
// _Profiles, then process the image. | ||
var newSrc = TryGetImageProfilePath(src, ext, width, height); | ||
imgTag = SetAttributeValue(imgTag, "src", newSrc); | ||
} | ||
catch (Exception ex) { | ||
Logger.Error(ex, "Unable to process Html Dynamic image profile for '{0}'", src); | ||
} | ||
} | ||
|
||
return imgTag; | ||
} | ||
|
||
private string TryGetImageProfilePath(string src, string ext, int width, int height) { | ||
var filters = new List<FilterRecord> { | ||
// Factor in a minimum width and height with respect to higher pixel density devices. | ||
CreateResizeFilter(width * Settings.DensityThreshold, height * Settings.DensityThreshold) | ||
}; | ||
|
||
if (_validExtensions[ext] != null && Settings.Quality < 100) { | ||
filters.Add(CreateFormatFilter(Settings.Quality, _validExtensions[ext])); | ||
} | ||
|
||
var profileName = string.Format( | ||
"Transform_Resize_w_{0}_h_{1}_m_Stretch_a_MiddleCenter_c_{2}_d_@{3}x", | ||
width, | ||
height, | ||
Settings.Quality, | ||
Settings.DensityThreshold); | ||
|
||
return _profileManager.GetImageProfileUrl(src, profileName, null, filters.ToArray()); | ||
} | ||
|
||
private FilterRecord CreateResizeFilter(int width, int height) { | ||
// Because the images can be resized in the HTML editor, we must assume that the image is of the exact desired | ||
// dimensions and that stretch is an appropriate mode. Note that the default is to never upscale images. | ||
var state = new Dictionary<string, string> { | ||
{ "Width", width.ToString() }, | ||
{ "Height", height.ToString() }, | ||
{ "Mode", "Stretch" }, | ||
{ "Alignment", "MiddleCenter" }, | ||
{ "PadColor", "" } | ||
}; | ||
|
||
return new FilterRecord { | ||
Category = "Transform", | ||
Type = "Resize", | ||
State = FormParametersHelper.ToString(state) | ||
}; | ||
} | ||
|
||
private FilterRecord CreateFormatFilter(int quality, string format) { | ||
var state = new Dictionary<string, string> { | ||
{ "Quality", quality.ToString() }, | ||
{ "Format", format }, | ||
}; | ||
|
||
return new FilterRecord { | ||
Category = "Transform", | ||
Type = "Format", | ||
State = FormParametersHelper.ToString(state) | ||
}; | ||
} | ||
|
||
private string ProcessImageAltContent(string imgTag) { | ||
var src = GetAttributeValue(imgTag, "src"); | ||
var alt = GetAttributeValue(imgTag, "alt"); | ||
|
||
if (string.IsNullOrEmpty(alt) && !string.IsNullOrEmpty(src)) { | ||
var text = Path.GetFileNameWithoutExtension(src).Replace("-", " ").Replace("_", " "); | ||
imgTag = SetAttributeValue(imgTag, "alt", text); | ||
} | ||
|
||
return imgTag; | ||
} | ||
|
||
private string GetAttributeValue(string tag, string attributeName) => | ||
_attributeValues | ||
.GetOrAdd($"{tag}_{attributeName}", _ => { | ||
var match = GetAttributeRegex(attributeName).Match(tag); | ||
return match.Success ? match.Groups[1].Value : null; | ||
}); | ||
|
||
private int GetAttributeValueInt(string tag, string attributeName) => | ||
int.TryParse(GetAttributeValue(tag, attributeName), out int result) ? result : 0; | ||
|
||
private string SetAttributeValue(string tag, string attributeName, string value) { | ||
var attributeRegex = GetAttributeRegex(attributeName); | ||
var newAttribute = $"{attributeName}=\"{value}\""; | ||
|
||
if (attributeRegex.IsMatch(tag)) { | ||
return attributeRegex.Replace(tag, newAttribute); | ||
} | ||
else { | ||
return tag.Insert(tag.Length - 1, $" {newAttribute}"); | ||
} | ||
} | ||
|
||
private Regex GetAttributeRegex(string attributeName) => | ||
_attributeRegexes.GetOrAdd( | ||
attributeName, | ||
name => new Regex($@"\b{name}\s*=\s*[""']?([^""'\s>]*)[""']?", RegexOptions.IgnoreCase | RegexOptions.Compiled)); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
...rchard.Web/Modules/Orchard.MediaProcessing/Handlers/MediaHtmlFilterSettingsPartHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using Orchard.ContentManagement; | ||
using Orchard.ContentManagement.Handlers; | ||
using Orchard.Environment.Extensions; | ||
using Orchard.Localization; | ||
using Orchard.MediaProcessing.Models; | ||
|
||
namespace Orchard.MediaProcessing.Handlers { | ||
[OrchardFeature(Features.OrchardMediaProcessingHtmlFilter)] | ||
public class MediaHtmlFilterSettingsPartHandler : ContentHandler { | ||
public MediaHtmlFilterSettingsPartHandler() { | ||
T = NullLocalizer.Instance; | ||
|
||
Filters.Add(new ActivatingFilter<MediaHtmlFilterSettingsPart>("Site")); | ||
Filters.Add(new TemplateFilterForPart<MediaHtmlFilterSettingsPart>( | ||
"MediaHtmlFilterSettings", | ||
"Parts.MediaProcessing.MediaHtmlFilterSettings", | ||
"media")); | ||
} | ||
|
||
public Localizer T { get; set; } | ||
|
||
protected override void GetItemMetadata(GetContentItemMetadataContext context) { | ||
if (context.ContentItem.ContentType != "Site") return; | ||
|
||
base.GetItemMetadata(context); | ||
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Media"))); | ||
} | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/MediaHtmlFilterSettingsPart.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
using Orchard.ContentManagement; | ||
|
||
namespace Orchard.MediaProcessing.Models { | ||
public class MediaHtmlFilterSettingsPart : ContentPart { | ||
public int DensityThreshold { | ||
get { return this.Retrieve(x => x.DensityThreshold, 2); } | ||
set { this.Store(x => x.DensityThreshold, value); } | ||
} | ||
|
||
public int Quality { | ||
get { return this.Retrieve(x => x.Quality, 95); } | ||
set { this.Store(x => x.Quality, value); } | ||
} | ||
|
||
public bool PopulateAlt { | ||
get { return this.Retrieve(x => x.PopulateAlt, true); } | ||
set { this.Store(x => x.PopulateAlt, value); } | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
...ediaProcessing/Views/EditorTemplates/Parts.MediaProcessing.MediaHtmlFilterSettings.cshtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
@model Orchard.MediaProcessing.Models.MediaHtmlFilterSettingsPart | ||
|
||
<fieldset> | ||
<legend>@T("Media Processing Filter")</legend> | ||
<div> | ||
@Html.LabelFor(m => m.DensityThreshold, T("Density Threshold")) | ||
@Html.DropDownListFor(m => m.DensityThreshold, new List<SelectListItem>(new[] { | ||
new SelectListItem { Value = "1", Text = "@1x" }, | ||
new SelectListItem { Value = "2", Text = "@2x" }, | ||
new SelectListItem { Value = "3", Text = "@3x" }, | ||
new SelectListItem { Value = "4", Text = "@4x" } | ||
})) | ||
<span class="hint">@T("The image will only be reduced if at least this pixel density is maintained.")</span> | ||
</div> | ||
<div> | ||
@Html.LabelFor(m => m.Quality, T("JPEG Quality")) | ||
@Html.TextBoxFor(i => i.Quality, new { @type = "number", @min = "0", @max = "100" }) % | ||
<span class="hint">@T("The quality level to apply on JPEG's, where 100 is full-quality (no compression).")</span> | ||
</div> | ||
<div> | ||
@Html.CheckBoxFor(i => i.PopulateAlt) | ||
@Html.LabelFor(m => m.PopulateAlt, T("Populate Empty Alt Tags").Text, new { @class = "forcheckbox" }) | ||
<span class="hint">@T("Populate an Alt tag based on the file name if the Alt tag is empty")</span> | ||
</div> | ||
</fieldset> |